Back

React 18-Alpha Is Out! This Is What You Need to Know

React 18-Alpha Is Out! This Is What You Need to Know

Announced in late 2020, React 17 didn’t bring many new features. Instead, it was focused on improving the fundamentals and laying the groundwork for future updates. All to enable seamless gradual adoption of what’s to come next.

That’s how we enter React 18. In a recent blog post, the React team announced the plan for the next version of React, alongside many of its upcoming features. There was also a release timeline, publicly available alpha, and even a Working Group dedicated to discussing and improving React 18.

Overall, a lot is going on with React, so let’s dive in and explore all the announcements with some additional details!

React 18 Working Group

Before we dive into the great new features, we need to discuss the React 18 Working Group (WG), as it’s the first time such concept has been used in React development.

The group is meant to provide feedback and prepare the broader ecosystem for the upcoming release of React. It’s limited to selected members, but as the conversation is hosted and made publicly available through GitHub Discussions, you can always check it out!

In Discussions, you’ll find different ideas, feature overviews, research findings from the React team, and much more, making it a goldmine of React 18 information.

Again, React 18 WG is the first such attempt to involve the community and ecosystem builders more in the creative process behind the next React release. How will it affect the end result? We’ll have to wait and see.

New Features

Unlike the prior version, React 18 looks to be filled with new features. Some are out-of-the-box improvements; others shine new light on React’s concurrent mode. Either way, there’s a lot to unpack, with potentially even more coming down the road!

Automatic Batching

Starting with performance, we’ve got huge improvements to automatic batching.

Before React 18, React already batched (aka grouped) multiple state updates into one to reduce unnecessary re-renders. However, it happened only in DOM event handlers, so Promises, timeouts, or other handlers didn’t take advantage of that.

const App = () => {
  const [firstCount, setFirstCount] = useState(0);
  const [secondCount, setSecondCount] = useState(0);
  const handleClick = () => {
    setFirstCount((count) => count + 1); // No re-render yet
    setSecondCount((count) => count + 0.5); // No re-render yet
    // Re-render (updates batched)
  };
  /*
  const alternativeHandleClick = () => {
    Promise.resolve().then(() => {
      setFirstCount((count) => count + 1); // Re-render
      setSecondCount((count) => count + 0.5); // Re-render
    });
  };
  */

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <span
        style={{
          color: firstCount % 2 === 0 ? "blue" : "black",
        }}
      >
        {secondCount}
      </span>
    </div>
  );
};

With v18, React will batch state updates no matter where they happen, as long as it’s safe to do so. This results in better performance without any additional involvement.

For more details, check out this WG thread to learn more about opting out of batching with ReactDOM.flushSync(), the edge-cases in older class-based components, and more.

Strict Mode Changes

Next up, some additions to the Strict Mode including new behavior called “strict effects”. This double-invokes effects - specifically mount and unmount ones.

The additional checks are here to test against multiple mount/unmount cycles. It ensures not only more resilient components but also correct behavior with Fast Refresh during development (when components are mounted/unmounted to be updated) and a new “Offscreen API”, which’s currently in the works.

Offscreen API will enable better performance by hiding components instead of unmounting them, keeping the state, and still calling the mount/unmount effects. That’ll play a crucial role in optimizing components like tabs, virtualized lists, etc.

For deeper dive on “Strict Effects”, check out this WG thread.

Root API

As for some user-facing API features, the render function - the one at the start of every React app, will be replaced by createRoot.

The new API is the gateway to accessing new React 18 features. It’s provided alongside the legacy API to encourage gradual adoption and ease-out potential performance comparisons.

New Root API is also a bit different syntax-wise.

import ReactDOM from "react-dom";
import App from "App";

const container = document.getElementById("app");

// Old
ReactDOM.render(<App />, container);

// New
const root = ReactDOM.createRoot(container);

root.render(<App />);

Notice how now the root of your React app is now separate. Root being the top-level data structure pointer attached to a DOM element, was previously opaque to the user of the render function.

You first have to create the root through the createRoot function and then call the render method on it. This not only enables new features but is also clearer and easier to update when necessary.

The new API also changes the way render callback and hydrate function work. The callback argument is completely removed due to difficulties in timing it properly. Instead, you should use e.g. timeout or ref callback on the root.

// ...

// Old
ReactDOM.render(<App />, container, () => console.log("renderered"));

// New
const App = ({ callback }) => {
  return (
    <div ref={callback}>
      <h1>Hello World</h1>
    </div>
  );
}
const root = ReactDOM.createRoot(container);

root.render(<App callback={() => console.log("renderered")} />);

As for hydrate, it turns from a separate function to a config option, making it more sensible.

// ...

// Old
ReactDOM.hydrate(<App />, container);

// New
const root = ReactDOM.createRoot(container, { hydrate: true });

root.render(<App />);

You can find a related WG thread here.

Concurrent Rendering

Without a doubt, the biggest update for React 18 comes with the “concurrent mode”, or as it’s now called - “concurrent rendering”.

The naming change is important, as it signifies the enabling of gradual adoption of concurrent features. This is meant to allow you to adopt concurrent features without rewrites, seamlessly, and at your own pace.

New APIs

With new features come new APIs. In the case of concurrent rendering, all of them are opt-in and cover things like state transitions, Suspense updates, and new hooks.

  • startTransition

React 18 introduces a new API to handle heavy state updates, like data fetches or list filtering. In these cases, simply setting the state would trigger immediate updates, possibly slowing down the UI.

To combat that, there’s a new API in the form of startTransition function. Its task is to mark state updates as transitions, making handling them non-urgent.

import { startTransition } from "react";

// ...

// Urgent -> show updated input
setInputValue(input);

// Transition (non-urgent) -> show search
startTransition(() => {
  setSearchQuery(input);
});

startTransition is really useful when you want to keep native user input snappy while doing complex, non-urgent operations in the background. If e.g. user updates the search query, the input field will reflect that instantly, whereas the previous transition will be canceled (if not already done), and a new one will be queued.

Apart from startTransition, a new useTransition hook will provide information about whether the given transition is pending or not and allow you to set an additional timeout for after the transition.

// ...

const App = () => {
  // ...

  const [isPending, startTransition] = useTransition({ timeoutMs: 2000 });

  startTransition(() => {
    setSearchQuery(input);
  });

  // ...

  return (
    <span>
      {isPending ? " Loading..." : null}
      {/* ... */}
    </span>
  );
};

// ...

You can then use this information to render loading UIs, implement custom handling, etc. For more information on transitions, how they compare to setTimeout, and other use-cases, check out this WG thread.

  • useDeferredValue

Continuing the trend of deferring value changes, we’ve got the new useDeferredValue hook.

import { useDeferredValue } from "react";

// ...

const [text, setText] = useState("text");
const deferredText = useDeferredValue(text, { timeoutMs: 2000 });

// ...

The hook will return a deferred version of the passed value, which will “lag behind” the original one for up to provided timeout.

It’s similar but a bit different from the transitions we’ve already discussed. Still, it’s helpful in situations where you need to implement complex state-based deferred behaviors.

  • SuspenseList

Lastly, there’s new <SuspenseList> component for organizing the order in which the directly-nested (1 level deep) <Suspense> and <SuspenseList> components get revealed.

import { Suspense, SuspenseList } from "react";

const App = () => {
  return (
    <SuspenseList revealOrder="forwards">
      <Suspense fallback={"Loading..."}>
        <ProfilePicture id={1} />
      </Suspense>
      <Suspense fallback={"Loading..."}>
        <ProfilePicture id={2} />
      </Suspense>
      <Suspense fallback={"Loading..."}>
        <ProfilePicture id={3} />
      </Suspense>
    </SuspenseList>
  );
};

// ...

The component is meant to handle cases where e.g. fetched data can arrive in an unpredictable order. Thanks to <SuspenseList>, it can be orchestrated and keep the UI in check.

More information, including how to adjust <SuspenseList> behavior with its props, can be found in Work-In-Progress (WIP) documentation.

Suspense Improvements

Combined with <SuspenseList> and other concurrent rendering updates are some improvements to the general Suspense behavior.

Suspense’s biggest advancement is finally becoming a stable feature, with big architectural changes under the hood and a new name - “Concurrent Suspense”. Still, the terminology is mostly for the purpose of differentiating the feature versions in the context of migration, as the user-facing changes are minimal.

The most significant user-facing change, while still minimal, has to do with the rendering of the suspended component’s siblings.

// ...

const App = () => {
  return (
    <Suspense fallback={<Loading />}>
      <SuspendedComponent />
      <Sibling />
    </Suspense>
  );
};

// ...

In React 17, the above code would result in <Sibling/> being immediately mounted and its effects called, only to be hidden shortly after.

React 18 fixes this by not mounting the component at all, waiting for <SuspendedComponent/> to resolve first. This fixes some issues with libraries in React ecosystem, though you shouldn’t notice that much.

For more details, read the following WG thread.

Streaming SSR

On top of the mentioned improvements to Suspense, we’ve also got huge changes to the Suspense Server-Side-Rendering (SSR) architecture.

These changes enable 2 major features - the use of <Suspense> and React.lazy(), alongside SSR streaming of rendered HTML, and selective hydration to start hydrating the app as early as possible.

Overall, a lot is going on, so for an additional overview, check out this thread. There’s also a detailed post on the architecture behind Suspense in SSR, so if you’re interested, that’s worth a read too!

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

Roadmap

So, with all these features, and likely more on the way, you’re probably interested when you’ll be able to use them.

The good news is you can use them now! Alpha is already available on NPM under the @alpha tag and is updated regularly. Still, it’s far, far from production-ready. You can take it for a spin if you’re curious, but know that bugs and issues are the expected norm.

Beyond that, the React team shared that beta will be available at least several months after that, with Release Candidate (RC) and general availability both spaced out several weeks.

This makes the earliest possible release date late this year (2021) and the latest - 1st half of 2022.

So, if you’re on the other side of the spectrum and don’t like constantly upgrading to new versions, then don’t worry. React 18 appears to be really smooth to upgrade to, and in addition to that, you still don’t have to worry about upgrades for a while.

Give It a Try

To finish up this post, let’s see what needs to be done to upgrade to React 18 right now. There are some great guides available already on the WG threads for both client and server, but to give you a quick overview:

Install

npm install react@alpha react-dom@alpha

Upgrade on client

  • Switch to the new Root API
  • Enable Strict Mode to see additional issues (optional)

Upgrade on server

  • Switch from renderToString (limited Suspense support) and renderToNodeStream (deprecated), to pipeToNodeWritable (new and recommended)

Bottom line

In summary, React 18 looks to be a great release. It’s feature-packed, easy to adopt, and gives the community more influence over its development process, thanks to the new Working Group.

Without a doubt, concurrent rendering is the biggest star of the show. With just a single change at the root of your React app, you get more flexibility and better performance. Along with new APIs, that’s the thing I’m looking forward to the most.