Welcome back, future AI-powered frontend wizard! In our previous chapters, we’ve explored the exciting world of consuming AI models and designing prompts. You’ve started to see how AI can bring incredible intelligence to your applications. But there’s a crucial aspect of real-world application development we haven’t deeply explored yet: time.
AI interactions, whether they’re calling a powerful cloud-based LLM or running a sophisticated model directly in the browser, are rarely instantaneous. They are asynchronous operations that involve waiting, much like fetching data from a traditional API or loading a large image. This waiting period introduces new challenges and opportunities for improving the user experience and the robustness of your application.
In this chapter, we’ll dive deep into making your AI-powered UIs resilient and user-friendly. You’ll learn how to effectively manage loading states, giving users clear visual feedback. We’ll also empower users (and your application!) with the ability to cancel ongoing AI requests, saving resources and preventing frustration. Finally, we’ll equip your applications with basic retry mechanisms, allowing them to gracefully recover from transient network glitches or temporary service interruptions. By the end of this chapter, you’ll have a solid understanding of how to handle the inherent asynchronous nature of AI, moving you closer to building truly production-ready applications.
Core Concepts: Mastering the Art of Waiting
Let’s break down the fundamental concepts that will help us tame the asynchronous beast of AI.
The Asynchronous Nature of AI: Why We Wait
Imagine you’re asking a super-smart assistant a complex question. It takes time for them to process the information, think, and formulate a detailed answer, right? Interacting with AI models is very similar.
- Network Latency (for API-based AI): When your React or React Native app sends a prompt to a cloud AI service (like OpenAI, Anthropic, Google AI, etc.), that request has to travel across the internet, be processed by the AI server, and then the response travels back. This round trip takes time, which can vary based on network conditions and server load.
- Model Loading & Inference (for In-Browser AI): If you’re running AI models directly in the browser using libraries like Transformers.js (which we’ll explore in a later chapter!), there’s an initial delay for the model to load into memory. Once loaded, the “inference” process (where the model generates a response) still takes computational time, especially for larger models or complex inputs.
Regardless of whether it’s an API call or local computation, your UI needs to gracefully handle these waiting periods.
Loading States: Keeping Users Informed
Have you ever clicked a button and nothing seemed to happen? It’s frustrating, right? Without visual feedback, users might:
- Click the button multiple times, sending duplicate requests.
- Assume the app is frozen or broken.
- Abandon the task entirely.
Loading states are visual cues (like spinners, progress bars, or disabled buttons) that inform the user that an operation is in progress. They improve the perceived performance of your application and prevent user frustration.
In React, we typically manage loading states using a simple useState hook, toggling a boolean flag (isLoading, isGenerating, etc.) before and after the async operation.
// Example of managing a loading state
const [isLoading, setIsLoading] = React.useState(false);
const [data, setData] = React.useState(null);
const fetchData = async () => {
setIsLoading(true); // Start loading
try {
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error("Failed to fetch:", error);
} finally {
setIsLoading(false); // End loading, regardless of success or failure
}
};
Cancellation: Empowering the User and Saving Resources
Sometimes, a user might change their mind while an AI request is pending. Maybe they spotted a typo in their prompt, or they simply don’t need the AI’s response anymore. Allowing them to cancel an ongoing request is a fantastic user experience improvement.
Beyond UX, cancellation offers practical benefits:
- Cost Savings: If you’re paying per token or per API call, cancelling a long-running AI generation can save you money, especially if the user doesn’t need the result.
- Resource Efficiency: Prevents your app from processing and displaying stale or unwanted data.
- Improved Responsiveness: Frees up network connections and processing power.
The modern web API for cancelling asynchronous operations is AbortController.
Introducing AbortController
The AbortController interface provides a way to signal and abort one or more Web requests or other asynchronous operations. It works like this:
- You create an
AbortControllerinstance. - It exposes a
signalproperty, which is anAbortSignalobject. - You pass this
signalto any cancellable asynchronous operation (likefetch). - When you want to cancel, you call the
abort()method on yourAbortControllerinstance. This dispatches anabortevent to thesignal, which then notifies any listeners (likefetch) to stop their operation.
Here’s a conceptual diagram of how it works:
When an operation is aborted, fetch (and many other modern APIs) will throw an AbortError. You can catch this error and handle it gracefully, often by simply ignoring it or showing a “Request cancelled” message.
Retries and Fallbacks: Building Resilience
The internet is a wild place. Network glitches, temporary server overloads, or rate limits are a fact of life. Instead of immediately throwing an error, your application can attempt to retry failed AI requests.
- Why retry? Many errors are transient. A second or third attempt might succeed.
- Basic Retry Logic: You can implement a simple loop that retries a request a fixed number of times with a delay between attempts.
- Exponential Backoff: A more advanced (and generally better) strategy is exponential backoff. This means the delay between retries increases exponentially (e.g., 1 second, then 2 seconds, then 4 seconds). This prevents overwhelming a potentially struggling server and gives it more time to recover.
If all retries fail, then you need a fallback. This could be:
- Displaying a user-friendly error message (“Sorry, AI is unavailable right now. Please try again later.”).
- Providing a default, non-AI-powered experience.
- Logging the error for later analysis.
Managing Async State in React
To implement loading, cancellation, and retries effectively, we’ll rely on React’s core state management tools:
useState: To hold ourisLoadingboolean, the AIresponse, anyerrormessages, and the userprompt.useEffect: Crucial for performing side effects (like initiating the AI request) and for cleaning up resources (like anAbortControllerinstance) when a component unmounts or dependencies change.useRef: Often useful for holding mutable references that don’t cause re-renders, such as theAbortControllerinstance itself, ensuring it persists across renders but can be updated without triggering a full component re-render.
Step-by-Step Implementation: An AI Assistant with Async Controls
Let’s build a simple React component that simulates an AI assistant. It will allow us to input a prompt, send it, show a loading state, enable cancellation, and even demonstrate retries.
First, let’s set up a basic React component. We’ll use a simulateAIResponse function to mimic an AI API call, introducing artificial delays and occasional errors.
// src/components/AIAssistant.jsx
import React from 'react';
// This function simulates an AI API call
const simulateAIResponse = async (prompt, signal) => {
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, Math.random() * 2000 + 1000)); // 1-3 seconds
// Simulate occasional network errors for retry demonstration
if (Math.random() < 0.3) { // 30% chance of failure
throw new Error("Simulated network error: Failed to reach AI service.");
}
// Check if the request was cancelled
if (signal && signal.aborted) {
throw new DOMException('Aborted', 'AbortError');
}
// Simulate AI processing
const responseText = `AI's insightful response to: "${prompt}".`;
return responseText;
};
function AIAssistant() {
// We'll add state here in the next steps
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: 'auto', border: '1px solid #ddd', borderRadius: '8px' }}>
<h1>AI Assistant</h1>
<p>Type your query and see the AI's response!</p>
{/* Input and buttons will go here */}
{/* Response area will go here */}
</div>
);
}
export default AIAssistant;
Explanation:
- We’ve created a
simulateAIResponseasync function. It takes apromptand anAbortSignal. - It uses
setTimeoutto mimic a network delay (between 1 and 3 seconds). Math.random() < 0.3introduces a 30% chance of throwing aSimulated network error, which will be crucial for testing our retry logic.- It explicitly checks
signal.abortedto throw anAbortErrorif cancellation occurs. This is howfetchand other cancellable APIs would behave. - The
AIAssistantcomponent is a basic React functional component with some styling.
Step 1: Basic AI Request with Loading State
Let’s add state to our AIAssistant component to manage the prompt, response, loading status, and any errors.
// src/components/AIAssistant.jsx (Updated)
import React, { useState } from 'react'; // Import useState
// ... simulateAIResponse function (unchanged) ...
function AIAssistant() {
const [prompt, setPrompt] = useState('');
const [response, setResponse] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const handleGenerateResponse = async () => {
if (!prompt.trim()) {
setError("Please enter a prompt.");
return;
}
setIsLoading(true); // Set loading to true
setError(null); // Clear previous errors
setResponse(''); // Clear previous response
try {
const aiResponse = await simulateAIResponse(prompt);
setResponse(aiResponse);
} catch (err) {
setError(err.message);
console.error("AI request failed:", err);
} finally {
setIsLoading(false); // Set loading to false, whether successful or not
}
};
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: 'auto', border: '1px solid #ddd', borderRadius: '8px' }}>
<h1>AI Assistant</h1>
<p>Type your query and see the AI's response!</p>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Ask the AI anything..."
rows="4"
style={{ width: '100%', padding: '10px', marginBottom: '10px', borderRadius: '4px', border: '1px solid #ccc' }}
/>
<button
onClick={handleGenerateResponse}
disabled={isLoading} // Disable button while loading
style={{
padding: '10px 20px',
backgroundColor: isLoading ? '#ccc' : '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: isLoading ? 'not-allowed' : 'pointer'
}}
>
{isLoading ? 'Generating...' : 'Generate AI Response'}
</button>
{isLoading && <p style={{ marginTop: '15px', color: '#007bff' }}>Loading...</p>} {/* Loading indicator */}
{error && <p style={{ marginTop: '15px', color: 'red' }}>Error: {error}</p>} {/* Error display */}
{response && (
<div style={{ marginTop: '20px', padding: '15px', backgroundColor: '#f9f9f9', border: '1px solid #eee', borderRadius: '4px' }}>
<strong>AI Response:</strong>
<p>{response}</p>
</div>
)}
</div>
);
}
export default AIAssistant;
Explanation:
- State Variables: We added
prompt,response,isLoading, anderrorusinguseState. handleGenerateResponse: This async function orchestrates the AI request.- It first checks if the prompt is empty.
- Sets
isLoadingtotrue, clears previouserrorandresponse. - Calls our
simulateAIResponse(without cancellation or retries yet). - Updates
responseorerrorbased on the outcome. - Sets
isLoadingback tofalsein thefinallyblock, ensuring it always resets.
- UI Updates:
- The “Generate” button is
disabledwhenisLoadingis true. - A “Loading…” message appears when
isLoadingis true. errormessages are displayed in red.- The
responseis shown when available.
- The “Generate” button is
Practice Challenge: Try interacting with the component. You’ll notice the “Generating…” message and the disabled button. Occasionally, you’ll see a simulated error. This is a good start, but we can do better!
Step 2: Implementing Request Cancellation with AbortController
Now, let’s add the ability to cancel an ongoing request. We’ll use AbortController and useRef to manage its instance.
// src/components/AIAssistant.jsx (Updated with Cancellation)
import React, { useState, useRef, useEffect } from 'react'; // Import useRef and useEffect
// ... simulateAIResponse function (unchanged) ...
function AIAssistant() {
const [prompt, setPrompt] = useState('');
const [response, setResponse] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
// useRef to hold the AbortController instance across renders
const abortControllerRef = useRef(null);
const handleGenerateResponse = async () => {
if (!prompt.trim()) {
setError("Please enter a prompt.");
return;
}
setIsLoading(true);
setError(null);
setResponse('');
// Create a new AbortController for this request
abortControllerRef.current = new AbortController();
const signal = abortControllerRef.current.signal;
try {
// Pass the signal to our simulated AI function
const aiResponse = await simulateAIResponse(prompt, signal);
setResponse(aiResponse);
} catch (err) {
// Check if the error is due to abortion
if (err.name === 'AbortError') {
setError("AI request cancelled by user.");
console.log("AI request aborted.");
} else {
setError(err.message);
console.error("AI request failed:", err);
}
} finally {
setIsLoading(false);
// Clear the ref after the request is complete (or cancelled)
abortControllerRef.current = null;
}
};
const handleCancelRequest = () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort(); // Trigger cancellation
}
};
// Cleanup useEffect: Abort any pending request if the component unmounts
useEffect(() => {
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []); // Empty dependency array means this runs once on mount and cleanup on unmount
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: 'auto', border: '1px solid #ddd', borderRadius: '8px' }}>
<h1>AI Assistant</h1>
<p>Type your query and see the AI's response!</p>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Ask the AI anything..."
rows="4"
style={{ width: '100%', padding: '10px', marginBottom: '10px', borderRadius: '4px', border: '1px solid #ccc' }}
/>
<div style={{ display: 'flex', gap: '10px', marginBottom: '15px' }}>
<button
onClick={handleGenerateResponse}
disabled={isLoading}
style={{
padding: '10px 20px',
backgroundColor: isLoading ? '#ccc' : '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: isLoading ? 'not-allowed' : 'pointer',
flexGrow: 1
}}
>
{isLoading ? 'Generating...' : 'Generate AI Response'}
</button>
{isLoading && ( // Show cancel button only when loading
<button
onClick={handleCancelRequest}
style={{
padding: '10px 20px',
backgroundColor: '#dc3545',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
flexGrow: 1
}}
>
Cancel
</button>
)}
</div>
{isLoading && <p style={{ marginTop: '15px', color: '#007bff' }}>Loading...</p>}
{error && <p style={{ marginTop: '15px', color: 'red' }}>Error: {error}</p>}
{response && (
<div style={{ marginTop: '20px', padding: '15px', backgroundColor: '#f9f9f9', border: '1px solid #eee', borderRadius: '4px' }}>
<strong>AI Response:</strong>
<p>{response}</p>
</div>
)}
</div>
);
}
export default AIAssistant;
Explanation:
abortControllerRef: We introduceuseRef(null)to store ourAbortControllerinstance.useRefis perfect here because we need to hold a mutable object that persists across renders, but changes to it shouldn’t trigger a re-render of the component itself.- Creating
AbortController: InsidehandleGenerateResponse, before the async call, we create a newAbortControllerand store it inabortControllerRef.current. We then get itssignal. - Passing
signal: Thesignalis passed tosimulateAIResponse. Our simulated function now correctly checkssignal.aborted. handleCancelRequest: This new function simply callsabortControllerRef.current.abort(), which triggers the cancellation.- Error Handling for Abort: In the
catchblock, we specifically checkerr.name === 'AbortError'. This allows us to show a user-friendly “cancelled” message instead of a generic error. - Cleanup
useEffect: This is critical. If theAIAssistantcomponent unmounts while a request is still pending, we must abort it to prevent memory leaks or unwanted state updates on an unmounted component. TheuseEffectwith an empty dependency array runs its cleanup function (return () => {...}) when the component unmounts.
Practice Challenge: Run the app, type a prompt, and click “Generate.” While it’s loading, quickly click “Cancel.” Observe the “AI request cancelled by user.” message. This is a big step towards a professional-feeling UI!
Step 3: Adding Basic Retry Logic
Now, let’s make our AI assistant more robust by implementing a basic retry mechanism for those simulated network errors.
// src/components/AIAssistant.jsx (Updated with Retries)
import React, { useState, useRef, useEffect } from 'react';
// ... simulateAIResponse function (unchanged) ...
function AIAssistant() {
const [prompt, setPrompt] = useState('');
const [response, setResponse] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const abortControllerRef = useRef(null);
const handleGenerateResponse = async () => {
if (!prompt.trim()) {
setError("Please enter a prompt.");
return;
}
setIsLoading(true);
setError(null);
setResponse('');
// Retry configuration
const maxRetries = 3;
const retryDelayMs = 1000; // 1 second delay
let attempts = 0;
let lastError = null;
while (attempts < maxRetries) {
attempts++;
abortControllerRef.current = new AbortController();
const signal = abortControllerRef.current.signal;
try {
// Display current attempt to the user (optional but good for debugging/UX)
if (attempts > 1) {
setError(`Retrying... Attempt ${attempts}/${maxRetries}`);
}
const aiResponse = await simulateAIResponse(prompt, signal);
setResponse(aiResponse);
lastError = null; // Clear error if successful
break; // Exit loop on success
} catch (err) {
// If cancelled by user, don't retry, just break
if (err.name === 'AbortError') {
setError("AI request cancelled by user.");
console.log("AI request aborted.");
lastError = null; // Clear error
break;
}
lastError = err; // Store the last error
console.error(`Attempt ${attempts} failed:`, err.message);
if (attempts < maxRetries) {
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, retryDelayMs));
}
} finally {
// Clear the ref for the *current* AbortController after each attempt
abortControllerRef.current = null;
}
}
setIsLoading(false);
// If after all retries, there's still an error (and not cancelled)
if (lastError) {
setError(`Failed after ${maxRetries} attempts: ${lastError.message}`);
}
};
const handleCancelRequest = () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
useEffect(() => {
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: 'auto', border: '1px solid #ddd', borderRadius: '8px' }}>
<h1>AI Assistant</h1>
<p>Type your query and see the AI's response!</p>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Ask the AI anything..."
rows="4"
style={{ width: '100%', padding: '10px', marginBottom: '10px', borderRadius: '4px', border: '1px solid #ccc' }}
/>
<div style={{ display: 'flex', gap: '10px', marginBottom: '15px' }}>
<button
onClick={handleGenerateResponse}
disabled={isLoading}
style={{
padding: '10px 20px',
backgroundColor: isLoading ? '#ccc' : '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: isLoading ? 'not-allowed' : 'pointer',
flexGrow: 1
}}
>
{isLoading ? 'Generating...' : 'Generate AI Response'}
</button>
{isLoading && (
<button
onClick={handleCancelRequest}
style={{
padding: '10px 20px',
backgroundColor: '#dc3545',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
flexGrow: 1
}}
>
Cancel
</button>
)}
</div>
{isLoading && <p style={{ marginTop: '15px', color: '#007bff' }}>Loading...</p>}
{error && <p style={{ marginTop: '15px', color: 'red' }}>Error: {error}</p>}
{response && (
<div style={{ marginTop: '20px', padding: '15px', backgroundColor: '#f9f9f9', border: '1px solid #eee', borderRadius: '4px' }}>
<strong>AI Response:</strong>
<p>{response}</p>
</div>
)}
</div>
);
}
export default AIAssistant;
Explanation:
- Retry Loop: We introduced a
while (attempts < maxRetries)loop withinhandleGenerateResponse. maxRetriesandretryDelayMs: Configurable variables for how many times to retry and the delay between attempts.- New
AbortControllerper Attempt: Crucially, a newAbortControlleris created for each retry attempt. This ensures that cancelling an earlier attempt doesn’t prevent subsequent retries from starting. - Error Handling &
lastError:- If an
AbortErroroccurs, webreakthe loop immediately, as the user explicitly cancelled. - For other errors, we store the
lastErrorand, ifattempts < maxRetries, we wait usingawait new Promise(resolve => setTimeout(resolve, retryDelayMs))before the next iteration. - After the loop, if
lastErroris still present, it means all retries failed, and we display a final error message.
- If an
finallyblock: TheabortControllerRef.currentis cleared after each attempt, ensuring it’s ready for the next one.
Practice Challenge:
Now, try generating responses multiple times. With the 30% failure rate, you should observe the “Retrying…” message before either succeeding or eventually failing after maxRetries. You can still cancel at any point during the retries!
Mini-Challenge: Exponential Backoff
You’ve implemented a fixed delay retry. Now, let’s make it smarter!
Challenge: Enhance the retry mechanism to use exponential backoff. Instead of a fixed retryDelayMs, make the delay increase with each failed attempt. A common pattern is initialDelay * (2 ^ (attempt - 1)). So, if initialDelay is 500ms:
- Attempt 1 (failed): wait 0.5s
- Attempt 2 (failed): wait 1s
- Attempt 3 (failed): wait 2s
- …and so on.
Hint:
- You’ll need an
initialRetryDelayMsconstant. - Inside the
whileloop, calculate the currentdelaybased onattempts. - Make sure to cap the maximum delay to prevent excessively long waits (e.g.,
Math.min(calculatedDelay, maxDelayMs)).
What to observe/learn:
- How to dynamically adjust delays in an asynchronous loop.
- A more robust and server-friendly retry strategy.
- Deeper understanding of
async/awaitwithsetTimeout.
Common Pitfalls & Troubleshooting
Forgetting
AbortControllerCleanup inuseEffect:- Pitfall: If a component unmounts while an AI request is pending, the
AbortController(and the pending promise) might still be active. If the request eventually resolves, it might try to update state on an unmounted component, leading to warnings or memory leaks. - Solution: Always include a
useEffectwith an empty dependency array ([]) and a cleanup function that callsabortControllerRef.current.abort()when the component unmounts. This is crucial for preventing unexpected behavior.
- Pitfall: If a component unmounts while an AI request is pending, the
Not Handling
AbortErrorSeparately:- Pitfall: If you don’t specifically check for
err.name === 'AbortError', a user cancellation will appear as a generic “Failed to fetch” or “Network error” message, which is confusing. - Solution: Always differentiate
AbortErrorin yourcatchblocks to provide specific feedback to the user (“Request cancelled”).
- Pitfall: If you don’t specifically check for
Infinite Retry Loops:
- Pitfall: Forgetting
maxRetriesor having a bug in your retry condition can lead to your application retrying indefinitely, consuming resources and potentially hitting API rate limits. - Solution: Always define a
maxRetrieslimit and ensure your loop correctly breaks on success or after reaching the maximum attempts.
- Pitfall: Forgetting
Race Conditions with Multiple Requests:
- Pitfall: If a user rapidly sends multiple prompts, responses might return out of order, or an earlier, slower response might overwrite a later, faster one.
- Solution: While our
AbortControllerhelps with the current request, for complex scenarios with many concurrent requests, consider more advanced state management libraries likeReact Query(orTanStack Query) orSWR. These libraries are purpose-built for managing asynchronous data fetching, caching, and handling race conditions gracefully. They are excellent tools for production-grade applications.
Ignoring UI Feedback:
- Pitfall: Not showing loading states, error messages, or cancellation confirmations leaves users in the dark.
- Solution: Always prioritize clear and immediate visual feedback for all async operations.
Summary
You’ve taken a significant leap forward in building robust, user-friendly AI applications! Let’s recap the key takeaways from this chapter:
- AI interactions are asynchronous: Expect delays due to network latency or local model processing.
- Loading states are essential for UX: Use
isLoadingflags and visual indicators (spinners, disabled buttons) to keep users informed and prevent frustration. AbortControllerenables cancellation: This modern browser API is your go-to for allowing users to cancel pending AI requests, saving resources and improving responsiveness. Remember to pass thesignalto your async operations and handleAbortErrorspecifically.- Retries build resilience: Implementing retry logic (especially with exponential backoff) helps your application recover from transient errors, making it more robust in real-world conditions.
- React’s
useState,useEffect, anduseRefare your allies: These hooks are fundamental for managing the state of your asynchronous operations and cleaning up resources.
By mastering these techniques, you’re not just writing code; you’re crafting experiences that are reliable, responsive, and truly intelligent.
In the next chapter, we’ll delve into even more advanced UI patterns for AI, building on these foundational async handling skills to create truly interactive and dynamic AI-powered interfaces.
References
- MDN Web Docs: AbortController: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
- React Official Documentation: State and Lifecycle: https://react.dev/learn/state-a-components-memory
- React Official Documentation: Hooks (useState, useEffect, useRef): https://react.dev/reference/react
- Hugging Face: Transformers.js (for in-browser AI context): https://huggingface.co/docs/transformers.js/index
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.