Concurrent React is the most striking feature of React 18. In React 17, rendering a component is synchronous, meaning nothing interrupts the flow when a component renders.
Assume we have a component called C. The diagram below shows the synchronous rendering of this component in which the green arrow represents the flow from rendering to committing the changes to the DOM so that the users can observe them.
In the synchronous rendering, the green arrow is not interruptible. However, this flow can be interrupted, suspended, scheduled, and continued in concurrent rendering.
Concurrent React can work on several tasks simultaneously and switch between them based on their priorities, which is why it can create some versions of your UI simultaneously in the memory. When you are using the concurrent features of React 18, such as useDefferedValue, startTransition, and Suspense, you are telling React that it must utilize its concurrent mechanism.
useDefferedValue
- When you have a slow component that is barely possible to optimize, we use the useDefferedValue along with React.memo for performance optimization.
It allows deferring the value that is not of high priority and instead doing some other tasks (rendering other components, for example) that are of high priority. When all the tasks of high priority are done, React renders the deferred value.
Assume you have a SlowComponent that takes a text and has to render many texts along with the new one given to it. There is also an input field that the user fills in to determine this text. To optimize the performance and make sure that SlowComponent will not block the user from typing, we do as shown below.
const SlowComponent = React.memo(function SlowComponent({ text }) {
// Does something with the text that is slow to render.
});
const App = () => {
const [text, setText] = useState("");
const deferredText = useDeferredValue(text);
return (
<React.Fragment>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowComponent text={deferredText} />
</React.Fragment>
);
};
Using deferredText does not make rendering SlowComponent faster. All it does is that it considers rendering SlowComponent of less priority so that it does not block the input field. React will try to render SlowComponent as soon as possible, but it will not block the user from typing. We must wrap SlowComponent inside memo so that this optimization works.
- We can use this hook to hide the
<Suspense>
fallback and display the stale data while the fresh data is loaded.
Pay attention that suspense will work if you are using suspense-enabled frameworks like Next.js or when you are lazy loading the code for the component.
In the example below, assume that the code of the app is lazy-loaded by using lazy.
import React, { useDeferredValue, useState } from "react";
const App = () => {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
return (
<React.Fragment>
<input value={query} onChange={e => setQuery(e.target.value)} />
<React.Suspense fallback={<p>loading...</p>}>
<SearchResults query={deferredQuery} />
</React.Suspense>
</React.Fragment>
);
};
When the user types something into the input field as a query, it gets fed to SearchResults to display data satisfying this query. When you input the letter, for example, b to the input field:
- At the first render, we have deferredValue = “b” and query = “b”
- When you enter a deferredValue = “b” and query = “ba”
So the query gets the new value immediately. However, deferredValue keeps its value till the SearchResults loads the data. In this scenario, SearchResults shows the stale data for a bit, instead of showing the Suspense fallback.
Transitions with startTransition
A transition is an interruptable state update. We use the useTransition hook to register a state update as a transition. The startTransition is similar to useDefferedValue, but it allows you to tell React which updates are a lower priority. A state update (state setter functions) used in the body of startTransition is considered a transition. These transitions will be interrupted by other state updates.
Now look at the example below. Assume there are two components called SlowComponent and FastComponent. And the user can choose one of them through the componentSelector function.
import React, { useState } from "react";
const App = () => {
const [componentSelected, setComponentSelected] = useState(null);
const componentSelector = (value) => setComponentSelected(value);
return (
<React.Fragment>
<button onClick={() => componentSelector("Fast")}>fast</button>
<button onClick={() => componentSelector("Slow")}>slow</button>
{componentSelected === "Fast" && <FastComponent />}
{componentSelected === "Slow" && <SlowComponent />}
</React.Fragment>
);
};
Now, if the user clicks the “slow” button and changes his mind and immediately clicks the “fast” button while SlowComponent is being rendered, it takes some time so that FastComponent renders, and it makes the UI not responsive for some time. This is because rendering here is not interruptable, and even if we do not need it to continue, it won't be suspended.
So here comes startTransition to solve this issue. When we register the setComponentSelected function that updates the state as a lower-priority task inside startTransition, React can interrupt its rendering. That is why when the user clicks the “slow” button and immediately selects the “fast” button, FastComponent will immediately render, and SlowComponent rendering will be interrupted and suspended.
import React, { useState, useTransition } from "react";
const App = () => {
const [componentSelected, setComponentSelected] = useState(null);
const [isPending, startTransition] = useTransition();
const componentSelector = (value) => {
startTransition(() => {
setComponentSelected(value); // Low priority, interruptable state update
});
}
return (
<React.Fragment>
<button onClick={() => componentSelector("Fast")}>fast</button>
<button onClick={() => componentSelector("Slow")}>slow</button>
{componentSelected === "Fast" && <FastComponent />}
{componentSelected === "Slow" && <SlowComponent />}
{isPending && <p>pending...</p>}
</React.Fragment>
);
};
Automatic batching
Batching is a performance optimization technique that React 17 (and other prior versions) used to group several state updates into one single re-render instead of re-rendering the component for each state update.
This batching was happening only inside React event handlers by default. And state updates inside of asynchronous code, like promises and setTimeout(), or any other events were not batched by default. Now React 18 automatically bathing batches the state updates in all of these.
Look at the code below.
const App = () => {
const [age, setAge] = useState("");
const [name, setName] = useState("");
const [lastName, setLastName] = useState("");
setTimeout(() => {
setAge(12);
setName("first name");
setLastName("last name");
}, 3000); // React 18 batches all the state updates and re-renders once only, unlike other versions
const eventHandler = () => {
setAge(12);
setName("first name");
setLastName("last name");
} // In React 18 and prior versions, these state updates inside the event handler, are batched automatically.
return (
// code
);
};
Assignment
Given the React component below, write the optimized code for this component. The optimized code must avoid any unnecessary component and child components re-render.
const Component = ({ bigText, userId }) => {
const [info, setInfo] = useState("");
const [userEmail, setUserEmail] = useState("");
useEffect(() => {
retrieveUserFromDB(userId)
.then(data => setUserEmail(data.user.email))
.catch(error => console.error(error))
});
const functionForChildComponent = e => setInfo(e.target.value);
const displayUserEmail = () => {
if (userEmail)
return <p>{userEmail}</p>
return null;
};
return (
<>
<ChildComponent setInfo={functionForChildComponent} info={info} />
<p>{bigText}</p>
{displayUserEmail()}
</>
);
};
To optimize this component, first, we must identify which parts of the component cause unnecessary re-renders. We list the issues below:
-
The useEffect dependency list is missing. It is essential to add a dependency list for useEffect to run only when one item in its DL changes.
- We add a DL and put userId in it to make the component re-render only if userId changes.
-
bigText makes this component re-render even if bigText has not changed.
- We “memorize” this value. We use React.memo() to memoize a component based on the props it receives.
const MemoizedBigText = React.memo(({ bigText }) => <p>{bigText}</p>);
MemoizedBigText re-renders if and only if props.bigText changes.
-
We are passing functionForChildComponent to the Child component. This makes React create a new function every time we render a Component.
- The good practice here is to memoize this function using useCallback so that it gets created only if any item inside its dependency list changes.
-
displayUserEmail is a pure function that receives some input and displays some data based on the input.
- We can again use the HOC (Higher Order Component) React.memo() to optimize it.
The revised code is:
const Component = ({ bigText, userId }) => {
const [info, setInfo] = useState("");
const [userEmail, setUserEmail] = useState("");
useEffect(() => {
retrieveUserFromDB(userId)
.then(data => setUserEmail(data.user.email))
.catch(error => console.error(error))
}, [userId]);
const functionForChildComponent = useCallback(e => setInfo(e.target.value), []);
return (
<>
<ChildComponent setInfo={functionForChildComponent} info={info} />
<MemoizedBigText bigText={bigText} />
<DisplayUserEmail userEmail={userEmail} />
</>
);
};
const MemoizedBigText = React.memo(({ bigText }) => <p>{bigText}</p>);
const DisplayUserEmail = React.memo(({ userEmail }) => {
if (userEmail)
return <p>{userEmail}</p>
return null;
});
The takeaway
All of these React.js changes are useful to know and implement by skilled developers. These latest changes make interacting with content much faster, and they make apps feel more polished. As there is room for improvement of course, for now it seems that all of these novelties in React.js do a pretty good job.