Using React in 2024

Watch the video on YouTube

React is constantly reinventing itself in order to stay ahead of the competition. You don’t believe me? Let’s review 7 of its most notable recent updates, and you’ll see that it is still relevant in this rapidly changing web environment.

Concurrent Mode

First, let’s look at Concurrent Mode. This is a behind the scenes update introduced in v18, which has major performance implications. Let me explain why.

Concurrent mode is an update of React’s core rendering model which now allows the framework to prepare multiple versions of your UI at the same time. Compare this with older versions where UI updates were packed in a single uninterrupted transition, which meant that when the rendering started, no event or action was able to interrupt it until the user saw the end result on the screen.

With concurrent rendering, this is not the case anymore. React may start rendering an update, pause in the middle to prioritize more important updates, then continue the paused update later.

Suspense

Thanks to this rework, features like Suspense are now more powerful.

<Suspense fallback={<Loading />}>
  <SomeComponent />
</Suspense>

The Suspense use case is straightforward - the component lets you display a fallback until all the code and data needed by the children has been loaded.

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

function Loading() {
  return <h2> Loading...</h2>;
}

Note however that Suspense can’t detect when data is fetched inside an Effect or event handler, so your child data sources have to be “suspense-enabled”. In short this is either a component marked as lazy, or a promise wrapped around the new experimental use() hook.

import { lazy } from "react";

const LazyAwesome = lazy(() => import("./AwesomeComponent.js"));

Use Transition

The new useTransition hook is another great result of this core rendering refactoring.

const [isPending, startTransition] = useTransition();

So you can now prioritize work loads in your UI, and render the parts of your app which are more important to the user first. The hook returns an isPending flag used to monitor if the workload was processed, and a startTransition function you can also use as standalone outside component boundaries.

function TabContainer() {
    const [search, setSearch] = useState();
    const [results, setResults] = useState([]);
    const [isPending, startTransition] = useTransition();

    function onSearchChange(ev) {
        setSearch(ev.target.value);
        startTransition(() => {
            setResults (filterData());
        });
    }

    return (<>
        <input
            type="text"
            value={search}
            onChange={onSearchChange}
        />
        {isPending && <>@</>}
        {!isPending && <ul>
            {result.map(it => (
                <li key={it.id}>{it.name}</li>
            ))}
        </ul>}
    </>);
}

In this example, I want users to have a smooth user experience while they are typing, so I am decoupling the rendering of the input value from other work that needs to be done as a result of the input change. I’m pretty sure we all end up building apps where keystrokes were lagging in the UI, and now we can easily fix such problems.

Use Optimistic

And since we are talking about great UX, let’s take a look at the new experimental useOptimistic hook. Thanks to this feature, we can now make our apps snappier by updating the UI in an optimistic manner.

Imagine we are implementing a Like button which also displays the current number of likes. We’ll use the count state value as the source for our optimistic hook, and then display it in the dom.

export function LikeButton({ count }) {
  const [optimistic, addOptimistic] = useOptimistic(
    count ?? 0,
    (state) => state + 1
  );

  function like() {
    addOptimistic(1);
    await postLike();
  }

  return <button onClick={like}>({optimistic})</button>;
}

When the user clicks on the button, the optimistic count will be increased, and the user will get some immediate visual feedback. Then, when the update is propagated to the server, the real value is updated, and all the syncing between the two values is performed behind the scenes.

Form Hooks

Next, let’s switch gears, and look at some new hooks aimed to improve our dev experience. Working with forms is a common practice in frontend development. While frameworks like Angular offer great support through their reactive forms implementation, we have to rely on 3rd party libraries when working with forms in React. However, with the new useFormState and useFormStatus hooks we can get more control over the various states of the form submission process, and we can better integrate the component state with the form action results.

function Form() {
  return (
    <form action={submitForm}>
      <input type="email" name="email" />
      <input type="password" name="password" />
      <LoginBtn />
    </form>
  );
}

function LoginBtn() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? "@" : "Login"}
    </button>
  );
}

Server Components

This wouldn’t be a React update article if we didn’t address the elephant in the room - react Server Components. This is yet another major effort aimed to optimize the way data is fetched from the server, computed, and presented in the UI.

Just like many others before it, React reached the same conclusion - moving Components on the server has some clear performance benefits ranging from being closer to the data source to being able to avoid passing external dependencies to the client for specific scenarios. Server components have to be combined with client components whenever interactivity is needed in the UI. For those keeping count, this is not SSR, so add yet another rendering strategy to the whole mix of options currently available.

The server can be leveraged not only for retrieving data. With server actions, functions annotated with “use server” can be passed as form submission handlers.

async function register(data) {
  "use server";
  service.register(data.get("email"));
}

function App() {
  return (
    <form action={register}>
      <input type="text" name="email" />
      <button type="submit">Register</button>
    </form>
  );
}

As a result we can now handle forms with zero javaScript to achieve quicker app load times and better performance over slow networks.

So React has a lot going for it. It is widely adopted, and enjoys a dev team dedicated to keeping the framework relevant. However, React is held back by a complicated reactivity model and a bloated codebase.