Fetching Data with React

Learn how to fetch data in React components using useEffect, handle loading states, and implement proper error handling.

Concept Guide

React Study Doc - Fetching Data

Mermaid Flowchart

Loading diagram...

useEffect

useEffect is a React Hook that lets you run side effects in your function components.

Side effects include:

  • data fetching
  • subscriptions
  • manually changing the DOM
  • timers
  • etc.
useEffect(setup, dependencies?)

Parameters

setup function
The first argument is a 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.

dependencies
The second argument is an 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.

cleanup function (optional)

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);
}, []);

My Code

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);
    }
  }
}
  • Setup function: runs setFilteredPosts(getFilteredAndSortedPosts()) if fetched is true.
  • Dependencies: [posts, search, sort, fetched]
    • The effect runs after the first render, and every time any of these change.

Handling Rendering States

There are 3 states:

  1. loading - data is loading
  2. error - fetch failed
  3. fetched - fetch succeeded
function 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>
)

Handling Rendering Component

Here we have a list that needs to be rendered.

We need to map over the array.

Mermaid Sequence Diagram

Loading diagram...

Interactive Example

Data Fetching Demo

This demonstrates the three states of data fetching: loading, error, and success.

Loading State
Error State
Error

Failed to fetch data

Success State
Success

Data loaded successfully

Further Exploration

Advanced Data Fetching Patterns

Custom Hooks for Data Fetching

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 };
}

Error Boundaries

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;
  }
}

Optimistic Updates

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 };
}

Data Fetching Architecture

Loading diagram...

Best Practices

  1. Always handle loading states - Users need feedback
  2. Implement error boundaries - Graceful error handling
  3. Use AbortController - Cancel requests when component unmounts
  4. Implement retry logic - Network failures are common
  5. Cache responses - Avoid unnecessary API calls
  6. Debounce search inputs - Reduce API calls during typing
// 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]);

React Query / TanStack Query

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>
  );
}

Suspense and Error Boundaries

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>
  );
}

Performance Considerations

  • Debouncing: Limit API calls during rapid user input
  • Caching: Store responses to avoid duplicate requests
  • Pagination: Load data in chunks for large datasets
  • Virtualization: Render only visible items for long lists
  • Background updates: Refresh data without blocking UI