With React 18 at RVA we have received a number of improvements related to performance optimization and smoother rendering. Some of the key new React 18 features that have been added include rendering changes and new hooks. In this article we will have a look at these changes and give examples.
Concurrent rendering
Concurrent rendering is a new feature in React that optimizes application performance by spreading work across multiple threads. This allows you to respond to user actions and data updates faster and more efficiently, even if it's in the middle of a heavy rendering task.
Before React 18, rendering was a single, continuous, synchronous operation, and once rendering started, it could not be interrupted. Concurrency is a fundamental update to the React rendering engine.
Example:
Imagine that a user enters text into a search box. The event updates the state of the component, and a new list of results is rendered. During this process, input jams: the browser cannot update the text entered the field as it is busy rendering the new result list. Competitive mode fixes this limitation and makes the tenderer intermittent.
Automatic batching
Automatic batching is a process whereby React groups by multiple states and/or proposes change into a single change operation, rather than having to update the component tree each time a change occurs. This can significantly improve application performance by reducing the number of redraw operations and reducing response times.
React automatically groups state change and/or props while processing events in the DOM, such as click or input events. For example, if multiple components update their state in response to a button click, React groups these changes and executes them simultaneously. So React doesn't redraw components after each change, but only after all the changes are complete. This avoids the extra cost of redrawing and optimizes application performance.
Example:
const App = () => {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const handleClick = () => {
// Before React 18, the following calls did not batch
// Setting state occurs "after" event in the callback of an asynchronous call.
fetchSomething().then(() => {
setCount((c) => c + 1); // Will provoke re-render
setFlag((f) => !f); // Will provoke re-render
});
// In React 18
fetchSomething().then(() => {
setCount((c) => c + 1); // Does not cause re-render
setFlag((f) => !f); // Does not cause re-render
// React will only call the re-render once, at the end
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
Transitions
Transitions can be used to indicate updates to the user interface that do not need urgent resource updates.
For example, two things happen when you type in an autocomplete field: a flashing cursor that shows visual feedback about the content you are typing and data search functionality in the background that searches for the data you are typing.
Displaying visual feedback to the user is important and therefore urgent. Searching is not as important and thus can be marked as non-urgent.
These non-urgent updates are called transitions. By marking non-urgent UI updates as "transitions", React will know which updates are a priority. This makes it easier to optimize rendering and get rid of obsolete rendering.
startTransition
You can mark updates as non-free using startTransition. Here is an example of what the AutoFill component will look like when using transitions:
import { startTransition } from 'react';
// Urgent update: display the entered text
setInputValue(input);
// Mark state updates as transitions
startTransition(() => {
// Go to: filter the list by the entered keyword
setSearchQuery(input);
});
startTransition is useful if you want to make user input fast, there was no UI freeze, and non-urgent operations were performed in the background.
useTransition
In addition to the startTransition there is a new hook useTransition. It will provide information on whether this transition is in progress and will allow you to set an additional timeout after the transition:
import { useTransition } from 'react';
const App = () => {
// ...
const [isPending, startTransition] = useTransition({ timeoutMs: 2000 });
startTransition(() => {
setSearchQuery(input);
});
// ...
return (
<span>
{isPending ? " Loading..." : null}
{/* ... */}
</span>
);
};
useDeferredValue
Hook will return the deferred version of the passed value, which will «lag» from the original by a time equal to the given timeout:
import { useDeferredValue } from "react";
// ...
const [text, setText] = useState("text");
const deferredText = useDeferredValue(text, { timeoutMs: 2000 });
This hook is useful in situations where we need to implement complex delayed behavior based on state.
How is Transitions different from debouncing or setTimeout?
- startTransition is executed immediately, unlike setTimeout.
- setTimeout has a guaranteed delay, while startTransition delay depends on the speed of the device and other urgent renderers.
- startTransition updates can be interrupted, unlike setTimeout, and do not freeze the page.
- React can track the waiting state for you if it is marked startTransition.
Suspense and SuspenseList
<Suspense> and <SuspenseList> allow easy control of asynchronous data loading and display of standby state.
<Suspense> is a wrapper component that allows you to set a waiting state for child components. When a child component renders, asynchronous data may not yet be loaded. In this case, <Suspense> displays fallback (backup) content while the data is being downloaded. When the data is downloaded, the child component will be displayed instead of fallback content:
import { Suspense } from 'react';
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<AsyncComponent />
</Suspense>
</div>
);
}
In this example,<Suspense> is used to wrap <AsyncComponent>. If the <AsyncComponent> is rendered but not yet loaded, the fallback, the component specified as the fallback attribute, is displayed.
The <SuspenseList> is a component that allows you to set the order in which multiple asynchronous components are loaded. When <SuspenseList> is used, react latest version can optimize the load order to display all components as quickly as possible and reduce the wait time for the user:
import { Suspense, SuspenseList } from 'react';
function MyComponent() {
return (
<div>
<SuspenseList revealOrder="forwards">
<Suspense fallback={<div>Loading...</div>}>
<AsyncComponent1 />
</Suspense>
<Suspense fallback={<div>Loading...</div>}>
<AsyncComponent2 />
</Suspense>
<Suspense fallback={<div>Loading...</div>}>
<AsyncComponent3 />
</Suspense>
</SuspenseList>
</div>
);
}
In this example, <SuspenseList> is used to specify the boot order for <AsyncComponent1>, <AsyncComponent2> and <AsyncComponent3> components. The revealOrder attribute specifies the order in which the component load will be displayed - in this example.
useId
useId is a new hook that allows you to generate unique identifiers within components without having to use third-party libraries or write your code to generate identifiers.
React previously used various methods to generate unique identifiers, such as random number generation or simple counters. However, this could lead to problems with possible conflicts of identifiers between different components.
useId provides a solution to this problem by allowing the generation of unique identifiers that do not conflict between different components.
Using React 18 useId is very simple — it is enough to call a hook and pass it some parameters, such as a prefix or a suffix, which will be used to generate the identifier. Hook returns a unique identifier that can be used for various purposes, such as element id attributes:
import { useId } from 'react';
function MyComponent() {
const inputId = useId('input-');
return (
<div>
<label htmlFor={inputId}>Input:</label>
<input type="text" id={inputId} />
</div>
);
}
useSyncExternalStore
useSyncExternalStore is a hook recommended for reading and subscribing to external data sources (repositories).
Signature:
const state = useSyncExternalStore(
subscribe, getSnapshot[, getServerSnapshot]
);
This method accepts three arguments:
- subscribe is a function for recording a callback that is called when the store status changes.
- getSnapshot is a function that returns the current value of the repository.
- getServerSnapshot is a function that returns a snapshot that is used during server rendering. This is an optional parameter.
This method returns the value of the storage state — state.
We create a useSyncExternalStore that reads since the current width of the browser window and displays it on the screen:
import { useSyncExternalStore } from 'react';
function App() {
const width = useSyncExternalStore(
//subscribe - Registers callback to listen for the browser window resize event.
(listener) => {
window.addEventListener('resize', listener);
return () => {
window.removeEventListener('resize', listener);
};
},
//getSnapshot - Returns current width of the browser window.
() => window.innerWidth,
//getServerSnapshot - It is used for server rendering, which is not required in this case, so it simply returns -1.
() => -1,
);
return <p>Size: {width}</p>;
}
export default App;
UseInsertionEffect
UseInsertionEffect appeared in React 18. It has the same signature as useEffect, but works synchronously in front of all DOM mutations, meaning it works for useLayoutEffect. It is used to embed styles into the DOM before reading the layout.
useInsertionEffect is designed for CSS-in-JS libraries such as styled-components. Since the scope of this hook is limited, it has no refs access and cannot schedule updates.
Strict mode
The Strict Mode in React 18 has improved. It adds a new mode called “strict effects”. Components wrapped in <StrictMode> (dev mode only) are intentionally rendered twice to avoid unwanted side effects that can be added during development. With “strict effects” the effects for newly mounted components are called twice (mount -> unmount -> mount). An additional effect call not only ensures that the component is running sustainably, but it is also necessary for Fast Refresh to work properly when components are mounted/unmounted during the upgrade development process.
Root API
The React team specifically left the old Root API so that users who have updated the version can gradually switch to the new one while comparing it with the old one. Using the old Root API will be accompanied by a warning in the console to switch to the new one. Consider the example with the new Root API and see the difference with the current implementation:
import * as ReactDOM from 'react-dom';
import App from 'App';
const container = document.getElementById('app');
// Before
ReactDOM.render(<App tab="home" />, container);
// After
const root = ReactDOM.createRoot(container);
root.render(<App tab="home" />);
Now separately created “root” – pointer to the top-level data structure, which React uses to track the tree for rendering. In previous versions of React the “root” was not available to the user, React attached it to the DOM node and did not return it anywhere. The new Root API changed the method of hydration of the container. Instead of hydrate you should write hydrateRoot:
import * as ReactDOM from 'react-dom';
import App from 'App';
const container = document.getElementById('app');
// Before
ReactDOM.hydrate(<App tab="home" />, container);
// After
// Creating and rendering with hydration
const root = ReactDOM.hydrateRoot(container, <App tab="home" />);
// Unlike createRoot(), you do not need to call root.render()
Note the order of arguments in hydrateRoot(): it accepts JSX by the second argument. This is because the client’s first renderer is special and requires matching with the server tree.
If you want to update the application "root" after hydration, you can save it to a variable and call render():
import * as ReactDOM from 'react-dom';
import App from 'App';
const container = document.getElementById('app');
// Create and render a root with hydration
const root = ReactDOM.hydrateRoot(container, <App tab="home" />);
// Updating application root
root.render(<App tab="profile" />);
Conclusion
Thus, React 18 for our work is a multifunctional, easy to use tool, and gives our team more influence on the development process.
If you still have questions, leave your contacts and our experts will contact you.