React is one of the most popular JavaScript libraries for creating user interfaces. There have been significant changes in its new version, and now I will tell you about the most important ones. 

React 18 can now work significantly better with applications containing big data. Using the concept of a virtual DOM and a new tool called parallel rendering, React effectively updates only the necessary components when changes occur, and this gives lightning-fast rendering speeds. 

There are new features such as Suspense, streaming server rendering and automatic batching. This allows all state changes that occur during an asynchronous operation to be updated at once.

My name is Igor Kramar, I am a senior front-end developer in the Kupol T1 team and have been working with React for several years. Today I will tell you about the most important changes in this program. 

Concurrent Mode

The most significant update that appeared in React 18 is the Concurrent Mode. This is a new mechanism with which you can prepare multiple versions of the user interface at the same time. It is designed to improve application performance and responsiveness, especially in cases where components take a long time to render. In its internal implementation, React uses complex techniques in this mode, such as priority queues and multiple buffering. 

Let’s look at what concurrent mode is, how it works, and how to use it in practice. 

What is concurrent mode?

This new mode of React operation allows the application to use CPU resources more efficiently and speeds up the process of rendering components. In concurrent mode, React can interrupt, pause, resume, or stop rendering. It works by breaking long operations into shorter ones and executing them in parts. This allows the browser to not be blocked and continue to display the user interface even during long operations. 

How does concurrent mode work?

Rendering is divided into several stages, which are performed one by one. At the same time, the system allows you to mark certain renderings as non-urgent. When rendering low-priority components, React periodically calls the main thread to determine whether there are more important tasks, such as user input. This makes rendering non-blocking and allows you to prioritize more important tasks. React ensures that the UI will look consistent even if rendering is interrupted. 

The system represents all elements present on the screen in the form of a tree structure – a tree of DOM (Document Object Model) components. The concurrent renderer in the background simultaneously creates another version of the tree in addition to the main one. 

Comparing these two trees gives an understanding of which elements need to be updated and which can be left as is. Concurrent Mode implements changes to the render and allows us to split (or pause) some updates, reduce UI blocking so that we have a more efficient interaction with the interface. After comparing the previous and current states of the tree components and determining changes, rendering occurs. 

That is, React can prepare new screens in the background without blocking the main thread. The browser is not blocked and continues to display the user interface even during long operations. 

How to enable concurrent mode?

React 18 introduces a new function, createRoot() , which provides the ability to create a root component concurrently. To do this, you need to pass the `concurrent: true` flag as the second argument when creating the root component: 

import { createRoot } from 'react-dom';

const root = document.getElementById('root');

createRoot(root, { concurrent: true }).render(<App />);

Now the application will work in concurrent mode. 

To use concurrent mode, you need to create a root component with the concurrent: true flag and use the useTransition() hook or the lazy()

function . Let’s look at a few code examples to understand how to use concurrent mode in practice. 

Dividing a long process into parts

Let’s say we have a LongProcess component that takes a long time to render: 

function LongProcess() {
  // Длинный процесс
  return <div>Long Process</div>;
}

To break this process down into parts, we can use the useTransition() hook:

import { useTransition } from 'react';

function LongProcess() {
  const [isPending, startTransition] = useTransition();

  function process() {
    // Длинный процесс
  }

  return (
    <div>
      {isPending ? 'Loading...' : 'Long Process'}
      <button onClick={() => startTransition(process)}>Start</button>
    </div>
  );
}

The useTransition() hook returns an array with two elements: isPending and startTransition(). isPending is a boolean value that indicates whether the process is currently running or not. startTransition() is the function we call to start the process. 

Lazy loading

Concurrent mode can also be used to lazy load components. To do this you need to use the lazy() function:

import { lazy, Suspense } from 'react';

const LazyComponent = lazy(() => import('./LazyComponent'));

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
      </Suspense>
    </div>
  );
}

The lazy() function takes a function that returns a promise with a component. When the component is ready, it will be rendered. Otherwise, the fallback component will be rendered. 

How the concurrent regime is applied in practice

This tool is most useful for applications where you need to work with a large amount of data, where the rendering of some components can cause significant delays. These can be dashboards, data presentation panels, complex forms, where, depending on the selected data in some fields, other fields or entire blocks can change. 

For example, in our company I work on the Dome-Administration project. This is an application that relates to the administration and security of IT systems. We display on a single screen all the information received from various equipment that is part of the company’s network. Each such device has its own admin panel, where you can view and change various parameters. And all this data in the form of complex forms and tables can be accumulated by the Dome-Administration system, where they need to be received, changed and drawn. 

This project is associated with a large amount of data that must be rendered, when some fields change in the table, something changes. Concurrent mode can significantly improve performance. And the gain is not in milliseconds, but in seconds. 

Another example is a CRM system for customer management, which contains a large amount of data. Concurrent Mode helps display information about customers and their orders more smoothly, even when the system processes data from tens of thousands of customers. The system renders data quickly and smoothly. This improves employee productivity because there is no need to wait for the screen to refresh. 

A similar benefit from this mode can be in interactive applications where many people are working simultaneously. For example, the Miro “interactive whiteboard” can have 15, 30 or even 50 employees working at the same time, changing something, moving sticky notes, drawing arrows, adding notes. They all interact with the server, and all changes must be quickly and simultaneously displayed to each participant. The concurrent mode mechanism makes it possible to work without delays and lags. 

How to understand that you need to use Concurrent Mode

To assess the demand for such a mode in an application, you need to look at the amount of data it receives from the backend server. If these are, relatively speaking, thousands of entities, a list of thousands of complex elements that are objects with their own properties, subobjects, then users may encounter delays in interaction and rendering. A concurrent regime can solve this problem if there are performance requirements from the business. 

Overall, Concurrent Mode is a powerful new feature in React, and most of the new features are built to take advantage of it. 

If the application is not very complex and the amount of data is not very large, then using this mode will not make the work faster. A simple application requires neither caching nor 
using Concurrent Mode. These are technologies for more complex and large companies that process large volumes of information. 

Automatic batching of state updates for asynchronous operations

This is another new feature in React 18 that allows you to automatically batch component state updates for asynchronous operations. This means that React will collect all state updates that occurred during the execution of an asynchronous operation and apply them in one overall update. 

How does automatic batching work?

It collects all state updates that occurred during the execution of an asynchronous operation and applies them in one overall update after the operation completes. This eliminates multiple component redraws and improves application performance. To use automatic batching, you need to use the setState() or useReducer() functions inside asynchronous operations such as setTimeout() or fetch(). 

Let’s look at a few code examples to understand how to use automatic batching in practice. 

Using setState() inside setTimeout()

An example where we use setState() inside setTimeout() . In this case, React automatically batches state updates that occur while setTimeout() is running : 

function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      setCount(count + 1);
      setCount(count + 1);
    }, 1000);
  }, [count]);

  return <div>Count: {count}</div>;
}

In this example, we use setTimeout() to set two state updates. But thanks to automatic batching, React will apply them in one overall update after setTimeout() completes . 

Using fetch()

An example where we use fetch() to fetch data and set a state update after fetching the data. In this case, React also automatically batches the state update: 

function App() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/todos/1')
      .then(response => response.json())
      .then(json => setData(json));
  }, []);

  return <div>{data ? data.title : 'Loading...'}</div>;
}

Thanks to automatic batching, React will apply this state update in one general update after fetch()completes . 

useDeferredValue hook

React 18 introduces a new hook, useDeferredValue , that allows you to defer updating a component’s state until it’s needed. This can be useful for improving performance and preventing unnecessary component redraws. To use useDeferredValue, you need to pass it a state value and a timeoutMs option to set the delay time before updating the value. 

How does useDeferredValue work?

The useDeferredValue hook works by creating a deferred version of a component’s state value. This deferred version will be used in place of the real value while the component is rendering, until the time comes when the value needs to be updated. 

When this happens, React compares the real value with the deferred version, and if they are different, the component is updated. 

Let’s look at some code examples to understand how to use useDeferredValue in practice. 

Using useDeferredValue with useState()

An example where we use useDeferredValue with useState() . In this case, we delay updating the state until the user has finished typing into the input field: 

function App() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text, { timeoutMs: 1000 });

  function handleChange(event) {
    setText(event.target.value);
  }

  return (
    <div>
      <input type="text" value={text} onChange={handleChange} />
      <div>Deferred text: {deferredText}</div>
    </div>
  );
}

In this example, we use useDeferredValue with useState() to lazy update the text state value. We also pass the option timeoutMs: 1000 to set the delay time in milliseconds before updating the value.

Using useDeferredValue with useReducer()

An example where we use useDeferredValue with useReducer() . In this case, we delay updating the state until the user has finished dragging the list items: 

function reducer(state, action) {
  switch (action.type) {
    case 'drag':
      return { ...state, draggedItem: action.payload };
    case 'drop':
      return { ...state, items: action.payload };
    default:
      return state;
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, {
    items: ['Item 1', 'Item 2', 'Item 3'],
    draggedItem: null,
  });

  const deferredState = useDeferredValue(state, { timeoutMs: 1000 });

  function handleDragStart(event, item) {
    dispatch({ type: 'drag', payload: item });
  }

  function handleDrop(event) {
    event.preventDefault();
    const newItems = state.items.filter(i => i !== state.draggedItem);
    newItems.splice(event.target.dataset.index, 0, state.draggedItem);
    dispatch({ type: 'drop', payload: newItems });
  }

  return (
    <ul>
      {deferredState.items.map((item, index) => (
        <li
          key={item}
          draggable
          onDragStart={event => handleDragStart(event, item)}
          onDrop={handleDrop}
          data-index={index}
        >
          {item}
        </li>
      ))}
    </ul>
  );
}

In this example, we use useDeferredValue with useReducer() to lazy update the state value . We also pass the option timeoutMs: 1000 to set the delay time in milliseconds before updating the value.

SSR improvements in React 18

React 18 introduces several changes to server-side rendering (SSR) that can significantly improve performance. Pre-rendering, using Suspense for SSR, and useOpaqueIdentifier are just some of the new features that can be useful when working with SSR. 

Let’s look at these changes and provide code examples for using them. 

Pre-rendering

React 18 includes new pre-rendering functionality that allows components to be rendered on the server before they are rendered on the client. 

This can significantly reduce page load time and improve SEO. 

To use pre-rendering in React 18, you can use a new method – renderToNodeStream() . 

Here’s a code example: 

import { renderToNodeStream } from 'react-dom/server';

app.get('/', (req, res) => {
  const stream = renderToNodeStream(<App />);
  res.write('<!DOCTYPE html>');
  res.write('<html>');
  res.write('<head>');
  res.write('<title>React 18 SSR</title>');
  res.write('</head>');
  res.write('<body>');
  stream.pipe(res, { end: false });
  stream.on('end', () => {
    res.write('</body>');
    res.write('</html>');
    res.end();
  });
});

The new server rendering function in React 18 renderToNodeStream() is used here to render the <App /> component on the server and pass it to the streaming response. We also add some basic HTML markup and close it after the streaming response completes. 

RenderToPipeableStream returns the pluggable Node.js stream. This feature integrates seamlessly with the Suspense feature and code sharing via React.lazy. Unlike renderToNodeStream, renderToPipeableStream provides a more flexible and optimized approach to server-side rendering, taking into account the new features of React 18. 

The renderToPipeableStream API provides a new mechanism for server-side rendering that is optimized to work with new React 18 features such as Suspense and React.lazy. This allows developers to create more responsive and performant web applications that better manage data loading and rendering of components on the server. 

Example of using renderToPipeableStream: 

import { renderToPipeableStream } from 'react-dom/server';
import App from './App';

const stream = renderToPipeableStream(<App />);

stream.pipe(res); // где 'res' является объектом ответа сервера

In this example, we import the renderToPipeableStream function from react-dom/server and use it to render our main App. We then pipe the result to the server response using the pipe method. 

The renderToPipeableStream API provides a new mechanism for server-side rendering that is optimized to work with new React 18 features such as Suspense and React.lazy. This allows developers to create more responsive and performant web applications that can better manage data loading and component rendering on the server. As a result, users get a faster, smoother user experience, and developers can more easily manage the complexity of their applications. 

Using Suspense for SSR

React 18 also includes improvements for using Suspense in SSR. You can use Suspense to wait for data to load on the server and display a stub before the data is available. 

Here’s a code example: 

import { Suspense } from 'react';
import { renderToString } from 'react-dom/server';

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <AsyncComponent />
      </Suspense>
    </div>
  );
}

app.get('/', async (req, res) => {
  const data = await fetchData();
  const html = renderToString(<App data={data} />);
  res.send(html);
});

In this example, we use Suspense to wait for data to load on the server before rendering the <AsyncComponent /> component. We also add a Loading… stub for display before the data is available. 

Using useOpaqueIdentifier for SSR

React 18 includes a new useOpaqueIdentifier hook that creates unique identifiers on the server and client. This can be useful for SSR when it is necessary to generate unique identifiers on the server for later use on the client. 

Here’s a code example: 

import { useOpaqueIdentifier } from 'react';

function App() {
  const id = useOpaqueIdentifier();
  return (
    <div>
      <h1>Component with ID: {id}</h1>
    </div>
  );
}

app.get('/', async (req, res) => {
  const html = renderToString(<App />);
  res.send(html);
});

In this example, we use useOpaqueIdentifier() to generate a unique identifier on the server when rendering the <App /> component . We also send the generated ID in the HTML code that will be used on the client. 

What are the key benefits of SSR that can impact the performance and user experience of web applications? 

Fast loading times: SSR updates only those parts of the HTML that need updating, allowing for fast switching between pages and very fast First Contentful Paint (FCP). Even users with slow internet connections or outdated devices can immediately interact with your web pages. 

Ease of Indexing: Indexing SSR sites is much easier for search engines than client-side rendered sites. Content is rendered before the page loads, so they don’t need to run JavaScript to read and index. 

Ideal for static websites: SSR is great for static web pages because it is faster to pre-render the static (or immutable) page on the server before sending it to the client. 

SSR updates only those parts of the HTML that need updating, resulting in fast switching between pages and very fast First Contentful Paint (FCP) even for users with very slow Internet connections or outdated devices. 

Additionally, indexing SSR sites is much easier for search engines than client-side rendered sites. Content is rendered before the page loads, so they don’t need to run JavaScript to read and index. In addition to all of the above, SSR is well suited for static web pages, since it takes much less time to render unchanged pages on the server before sending them to the client. 

useId hook

React 18 includes a new useId hook that helps you create unique IDs on the server and client for DOM elements. This can be useful when unique identifiers are needed for elements that will be used as links, for event processing, or when working with forms. 

Here’s a code example: 

import { useId } from 'react';

function App() {
  const inputId = useId();
  const labelId = useId();
  return (
    <div>
      <label htmlFor={inputId}>Name:</label>
      <input id={inputId} type="text" />
    </div>
  );
}

app.get('/', async (req, res) => {
  const html = renderToString(<App />);
  res.send(html);
});

In this example, we use useId() to generate unique IDs for the <input> and <label> elements . We also use htmlFor to associate the label with the input field. 

You can pass a prefix as an argument to useId() to create unique ids with a specific prefix: 

const inputId = useId('input');
const labelId = useId('label');

In this example, we create unique identifiers with the prefixes <input> and <label> . 

useSyncExternalStore hook

React 18 includes a new useSyncExternalStore hook that allows you to synchronize the state of a component with an external data store. This can be useful when you need to exchange data between components or with other applications. 

Here’s a code example: 

import { useSyncExternalStore } from 'react';

function App() {
  const [count, setCount] = useState(0);
  useSyncExternalStore('count', count, setCount);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

In this example, we use useSyncExternalStore() to synchronize the state of count with an external data store. We pass the variable name count , the value of count , and the setCount function to update the value of count . 

You can use useSyncExternalStore() to synchronize any variables or objects: 

const [user, setUser] = useState({ name: 'John', age: 30 });
useSyncExternalStore('user', user, setUser);

In this example, we are synchronizing the user object with an external data store. 

You can also use useSyncExternalStore() to exchange data between components: 

// Component A
const [value, setValue] = useState('');
useSyncExternalStore('value', value, setValue);

// Component B
const [value, setValue] = useState('');
useSyncExternalStore('value', value, setValue);

In this example, we are synchronizing the value between two components. 

We recommend using React’s built-in state management hooks, such as useState and useReducer , to manage state. However, there are scenarios where useSyncExternalStore makes sense too: 

  • Integrating React with existing non-React code.
  • Subscribe to a browser API (for example, web notifications or the navigator.onLine property).

useInsertionEffect hook

One of the improvements in React 18 is the new useInsertionEffect hook , which makes it possible to perform effects after a component is mounted, but before it is rendered. This can be useful for performing actions that need to be done only once after mounting a component. 

Here’s a code example: 

import { useInsertionEffect } from 'react';

function App() {
  const [count, setCount] = useState(0);

  useInsertionEffect(() => {
    console.log('Component has been mounted');
  });

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

In this example, we use useInsertionEffect() and pass in a function that will be executed after the component is mounted. 

You can use useInsertionEffect() to perform any actions that need to be done after the component is mounted: 

useInsertionEffect(() => {
  // Выполнить запрос к API
  // Загрузить данные в состояние компонента
  // И т. д.
});

You can also use useInsertionEffect() to perform actions before each component renders: 

useInsertionEffect(() => {
  console.log('Component will be re-rendered');
}, [count]);

In this example, we pass the [count] dependency so that the effect will only be executed before each render of the component if the value of count has changed. 

What’s next?

We are currently working on React Optimizing Compiler (ROC), a new tool that will help optimize application performance by speeding up the rendering process of components and reducing page load times. This compiler automatically generates the equivalent of useMemo and useCallback calls, thereby minimizing the cost of re-rendering without losing the core React programming model. 
ROC uses various optimization techniques: code compression, removal of unused dependencies, and dynamic loading of components. This allows you to reduce the size of the application and speed up its operation on mobile devices. 

In mid-June 2022, the React Core team completed the ROC rewrite. One of the key features of the new version is the ability to analyze and remember complex patterns, such as local mutations. This opens the door to many new compile-time optimization possibilities that were previously unavailable. 

ROC will be available as part of React Developer Tools, which help developers debug and optimize applications. 

Overall, React Optimizing Compiler is a powerful tool for optimizing the performance of React applications. It will help speed up applications and improve user experience. Developers can use ROC to create fast and efficient React applications. 

In future versions, React developers promise: 

Asset Loading. Future versions of React will improve handling of loading resources such as images, fonts, styles, and other files. This will improve the performance and speed of applications, especially on mobile devices. 

React will add new APIs to handle resource loading, such as preload and prefetch methods for preloading resources that will be used in the future. This will also help speed up the application and reduce user waiting time. 

Another new tool for working with resource loading that is already present in React 18 is React.lazy. It allows components to be lazily loaded when needed, which reduces page load time and improves application performance. 

In addition, React will add new features for working with images, for example, a Picture component for adaptive loading of images depending on the screen size of the user’s device. It will also add support for the WebP image format, which has a higher compression ratio and loads faster on mobile devices. 

Overall, improvements to resource loading in future versions of React will help speed up applications and improve the user experience. 

Offscreen is a new API that will be introduced in future versions of React. You can render components off-screen, which improves application performance and reduces page load times. 

How it works? When a component is off screen, it is not rendered. Instead, Offscreen creates a hidden area where the component can be rendered without being displayed on screen. When a component becomes visible, it quickly appears on the screen. 

Offscreen allows you to improve application performance, especially on mobile devices. It can also be useful for components that don’t need to be constantly updated but still need to be available on the page. 

Additionally, Offscreen uses lazy loading, which means components will only load when they become visible on the screen. This helps reduce the size of the application and speed up its performance. 

Offscreen will help speed up applications and improve user experience. 

Developers can use Offscreen to create fast and efficient React applications. 

Server Components is a new approach to creating React applications, which will be introduced in future versions of the library. Thanks to it, you can separate components into two parts: client and server. The client part is responsible for displaying the component on the screen, and the server part is responsible for its preliminary processing on the server side. 

How it works? When a user requests a page, the backend component is rendered on the server side and sent to the user as a finished HTML page. 

The client side of the component is then loaded and linked to the server side to provide interactivity and dynamic behavior. 

Server Components can improve application performance, especially when working with large amounts of data and complex components. They can also be useful for creating fast and responsive applications on slow devices or with poor connections. 

Additionally, Server Components can be used to create universal applications that can run on both the client and server sides. This will improve SEO optimization and speed up page load time. 

Server Components will be available as part of React Developer Tools and will be supported by Chrome and Firefox browsers. They will also be integrated with popular frameworks and libraries such as Next.js and Gatsby. 

Transition Tracing is a new tool expected in future versions of React to make it easier to debug and optimize animations and transitions in your application. Developers can easily track and analyze the transition process between different states of components. 

How it works? Transition Tracing records all component state changes and creates a run-time diagram that shows which components were updated and which animations were run during the transition. This enables developers to quickly identify performance issues and optimize code to improve the user experience. 

In addition, Transition Tracing allows developers to easily customize animation and transition parameters: duration, delay, and effects. This helps create smoother, more efficient animations that won’t slow down your application. 

Transition Tracing can be especially useful for creating complex and interactive user interfaces that require a lot of animations and transitions. It can also be useful for creating applications that need to run on slow devices or with poor connections. 

Additionally, Transition Tracing can be used to create universal applications that can run on both the client and server sides. This will improve SEO optimization and speed up page load time.