Learn how to fetch data in React components using useEffect, handle loading states, and implement proper error handling.
Loading diagram...
useEffect
is a React Hook that lets you run side effects in your function components.
Side effects include:
useEffect(setup, dependencies?)
function
.
This function runs after the component renders.
This function has your Effect's logic.
Your setup function may also optionally return a cleanup function.
When your component is added to the DOM, React will run your setup function. After every re-render with changed dependencies, React will first run the cleanup function (if you provided it) with the old values, and then run your setup function with the new values.
After your component is removed from the DOM, React will run your cleanup function.
array of variables
.
The effect will re-run whenever any value in this array changes.
If you pass an empty array ([]
), the effect runs only once after the initial render (like componentDidMount
).
If you omit the array, the effect will run after every render.
If your linter is configured for React, it will verify that every reactive value is correctly specified as a dependency.
The list of dependencies must have a constant number of items and be written inline like [dep1, dep2, dep3].
React will compare each dependency with its previous value using the Object.is comparison. If you omit this argument, your Effect will re-run after every re-render of the component.
If your effect needs to clean up (e.g., remove a timer, unsubscribe), return a function from the setup function:
useEffect(() => { const timer = setInterval(() => { // do something }, 1000); // Cleanup function runs before the effect re-runs or when the component unmounts return () => clearInterval(timer); }, []);
const [fetched, setFetched] = useState(false); useEffect(() => { if (fetched) { setFilteredPosts(getFilteredAndSortedPosts()); } }, [posts, search, sort, fetched]); function App() { const [posts, setPosts] = useState([]); const [filteredPosts, setFilteredPosts] = useState([]); const [search, setSearch] = useState(''); const [sort, setSort] = useState('none'); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [fetched, setFetched] = useState(false); // Fetch posts only when button is clicked async function fetchPosts() { setLoading(true); setError(''); try { const res = await fetch('https://dummyjson.com/posts'); if (!res.ok) throw new Error('Failed to fetch posts'); const data = await res.json(); setPosts(data.posts); setFilteredPosts(data.posts); setFetched(true); } catch (err) { setError(err.message); } finally { setLoading(false); } } }
setFilteredPosts(getFilteredAndSortedPosts())
if fetched
is true.[posts, search, sort, fetched]
There are 3 states:
loading
- data is loadingerror
- fetch failedfetched
- fetch succeededfunction Spinner() { return <div style={{ padding: "2em", textAlign: "center" }}>Loading...</div>; function ErrorMessage({ message }) { return ( <div> <p style={{ color: "red", padding: "2em" }}>{message}</p> </div> ); } return( /* other sections */ <section> {loading ? ( <Spinner /> ) : error ? ( <ErrorMessage message={error} /> ) : ( fetched && ( <List posts={filteredPosts} search={search} sort={sort} setSort={setSort} /> ) )} </section> )
Here we have a list that needs to be rendered.
We need to map over the array.
Loading diagram...
This demonstrates the three states of data fetching: loading, error, and success.
Failed to fetch data
Data loaded successfully
Create reusable hooks to manage data fetching logic:
function useDataFetching(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const fetchData = useCallback(async () => { setLoading(true); setError(null); try { const response = await fetch(url); if (!response.ok) throw new Error('Network response was not ok'); const result = await response.json(); setData(result); } catch (err) { setError(err.message); } finally { setLoading(false); } }, [url]); useEffect(() => { fetchData(); }, [fetchData]); return { data, loading, error, refetch: fetchData }; }
Implement error boundaries to catch and handle errors gracefully:
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { console.log('Error caught by boundary:', error, errorInfo); } render() { if (this.state.hasError) { return <h1>Something went wrong.</h1>; } return this.props.children; } }
Update UI immediately while making API calls:
function useOptimisticUpdate() { const [data, setData] = useState(null); const updateOptimistically = useCallback( async (newData, updateFn) => { // Update UI immediately setData(newData); try { // Make API call await updateFn(); } catch (error) { // Revert on error setData(data); throw error; } }, [data] ); return { data, updateOptimistically }; }
Loading diagram...
// Example with AbortController useEffect(() => { const abortController = new AbortController(); const fetchData = async () => { try { const response = await fetch(url, { signal: abortController.signal, }); const data = await response.json(); setData(data); } catch (error) { if (error.name === 'AbortError') { // Request was cancelled return; } setError(error.message); } }; fetchData(); return () => { abortController.abort(); }; }, [url]);
For more advanced data fetching, consider using React Query:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; function PostsComponent() { const { data: posts, isLoading, error, } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, staleTime: 5 * 60 * 1000, // 5 minutes }); const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: createPost, onSuccess: () => { // Invalidate and refetch queryClient.invalidateQueries({ queryKey: ['posts'] }); }, }); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; return ( <div> {posts.map((post) => ( <div key={post.id}>{post.title}</div> ))} </div> ); }
React 18+ supports Suspense for data fetching:
function App() { return ( <ErrorBoundary fallback={<ErrorFallback />}> <Suspense fallback={<LoadingSpinner />}> <PostsList /> </Suspense> </ErrorBoundary> ); } function PostsList() { const posts = use(fetchPosts()); // use() hook for promises return ( <div> {posts.map((post) => ( <Post key={post.id} post={post} /> ))} </div> ); }