DEV Community

Cover image for Understanding Concurrency in React: A Guide to Smoother and More Responsive UIs
Rinon Tendrinomena
Rinon Tendrinomena

Posted on

Understanding Concurrency in React: A Guide to Smoother and More Responsive UIs

As web apps get more complex, keeping them fast and smooth for users can be tough. That’s where concurrency in React comes in. It helps React manage multiple tasks at once, making your app run better and feel more responsive.

What Does Concurrency Mean in React?

My short definition is of Concurrency in React is that React can handle several things at the same time. This is especially important with new features like Automatic Batching and Transitions, which help make your app feel smoother.

Let's take a look at the key ideas behind Concurrency:

1. Concurrent Rendering:
With concurrent rendering, React can handle multiple updates at the same time. If something more urgent happens, like a user clicking a button or switching between tabs, React can pause or interrupt one update to focus on the more important task. For example, if a user accidentally clicks on one tab and then quickly switches to another, React won't wait for the first tab to finish loading. Instead, it will switch to the most recent tab, keeping the app responsive and fast. Check the example made by the React team here.

2. Automatic Batching:
In React 18, automatic batching allows React to group several state changes together and apply them all in one go. This means that instead of updating the app for each individual state change, React combines them into a single update. This reduces the number of renders, making the app more efficient and faster.

Examples:
a. Form Inputs:
Suppose you have a form with multiple inputs. If you change the value of several inputs quickly, React will batch these changes into a single update. Without automatic batching, each input change might trigger a separate re-render, slowing down the app.

function MyForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  const handleChange = (e) => {
    setName(e.target.value);
    setEmail('newemail@example.com');
  };

  return (
    <div>
      <input value={name} onChange={handleChange} />
      <input value={email} onChange={handleChange} />
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

In this example, both setName and setEmail will be batched together, so React will only re-render once, instead of twice.

b. Async Operations:
If you're handling multiple async operations, such as fetching data and updating state based on the results, automatic batching can combine these updates into one render.

import { useState, useEffect } from 'react';

function DataFetcher() {
  const [data, setData] = useState(null); // State to store fetched data
  const [loading, setLoading] = useState(true); // State to track loading status
  const [error, setError] = useState(null); // State to handle errors
  const [retry, setRetry] = useState(false); // State to trigger a retry

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true); // Start loading
      setError(null); // Clear previous errors

      try {
        const response = await fetch('/api/data');

        if (!response.ok) {
          throw new Error('Network response was not ok');
        }

        const result = await response.json();
        setData(result); // Update data state with the fetched result
      } catch (error) {
        setError(error.message); // Set error state if fetch fails
      } finally {
        setLoading(false); // End loading
      }
    };

    fetchData();
  }, [retry]); // Dependency array includes retry to re-fetch data when retry changes

  const handleRetry = () => {
    setRetry(prevRetry => !prevRetry); // Toggle retry state to re-trigger fetch
  };

  return (
    <div>
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error}</p>}
      {data && <p>Data: {JSON.stringify(data)}</p>}
      {!loading && !error && !data && <p>No data available</p>}
      {error && <button onClick={handleRetry}>Retry</button>}
    </div>
  );
}

export default DataFetcher;

Enter fullscreen mode Exit fullscreen mode

During the data fetching process, several state updates occur:

  • setLoading(true) before starting the fetch.
  • setError(null) to clear any previous errors.
  • setData(result) after a successful fetch.
  • setError(error.message) if an error occurs.
  • setLoading(false) to signal that loading is complete.

Automatic batching combines these updates into a single render cycle. This means React processes these changes all at once, avoiding multiple renders.

3. Transitions:
Transitions let you mark some updates as less urgent. For example, when moving to a new page, React can keep showing the current page while it prepares the new one in the background. This makes the app feel smoother.

Examples of Transitions:
a. Page Navigation:
When navigating between pages, React can mark the navigation as a transition. This means React will keep showing the current page while it loads and prepares the new page in the background.

import { useTransition, useState } from 'react';

function App() {
  const [isPending, startTransition] = useTransition();
  const [page, setPage] = useState('home');

  const goToPage = (newPage) => {
    startTransition(() => {
      setPage(newPage); // Mark page change as a transition
    });
  };

  return (
    <div>
      <button onClick={() => goToPage('home')}>Home</button>
      <button onClick={() => goToPage('about')}>About</button>
      <button onClick={() => goToPage('contact')}>Contact</button>

      {isPending ? <p>Loading...</p> : <PageContent page={page} />}
    </div>
  );
}

function PageContent({ page }) {
  if (page === 'home') return <p>Home Page</p>;
  if (page === 'about') return <p>About Page</p>;
  if (page === 'contact') return <p>Contact Page</p>;
  return null;
}

Enter fullscreen mode Exit fullscreen mode

In this example, clicking on different buttons triggers a page change. React will handle the page change as a transition, keeping the current page visible while preparing the new one.

b. Filtering a List:
When applying filters to a list, you can mark the filtering operation as a transition. This way, React can keep showing the current list while it processes and applies the new filter criteria in the background.

import { useTransition, useState } from 'react';

function ItemList() {
  const [isPending, startTransition] = useTransition();
  const [filter, setFilter] = useState('');
  const [items, setItems] = useState(allItems);
  const allItems = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];

  const handleFilterChange = (e) => {
    const newFilter = e.target.value;
    startTransition(() => {
      setFilter(newFilter);
      const filteredItems = allItems.filter(item => item.includes(newFilter));
      setItems(filteredItems);
    });
  };

  return (
    <div>
      <input value={filter} onChange={handleFilterChange} placeholder="Filter items" />
      {isPending ? <p>Filtering...</p> : <ul>{items.map(item => <li key={item}>{item}</li>)}</ul>}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, as you type in the filter input, React marks the filtering process as a transition. The list will update in the background, while the UI remains responsive.

4. Suspense:
Suspense lets React pause part of the UI while it waits for data to load. With concurrency, React can still update other parts of the UI, so users aren’t stuck waiting for everything to load.

Example:(From React docs)

import { Suspense } from 'react';
import Albums from './Albums.js';

export default function ArtistPage({ artist }) {
  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<Loading />}>
        <Albums artistId={artist.id} />
      </Suspense>
    </>
  );
}

function Loading() {
  return <h2>🌀 Loading...</h2>;
}
Enter fullscreen mode Exit fullscreen mode

In this example, pretend we were fetching the artist details in the Albums component. While resolving the promise for getting the data, the UI will display a loading text. This means that users don't see a black page/UI while waiting for the artist's details to be loaded.

5. Priority Levels:
React gives different tasks different priority levels. Important tasks, like handling user input, are done first, while less important tasks, like background data loading, are done later. This keeps the app fast and responsive.

Concurrency in React helps keep your app responsive and fast, even as it gets more complex. By using these features, you can build apps that feel quick and smooth, giving users a better experience.

That's it! As always thanks for reading and I hope you learned more about Concurrency in React. There are more features I didn't write about yet as I don't this post to be very long.

Your comments and feedback are valuable to me! If you have any suggestions, corrections, or improvements, please feel free to share them.

Top comments (2)

Collapse
 
tarunkumarkale profile image
Tarun kale

This is really helpful. Recently, I completed (not fully, at least 95%) using React, Tailwind CSS for design, and Firebase for data collection. Actually, this website is a platform where users can buy, rent, and sell e-cycles.

Collapse
 
rinonten profile image
Rinon Tendrinomena

Thanks for sharing @tarunkumarkale! We'd love to see the platform once it's live