Concurrency is an important change in React 18
.
I'm going to look at the following hooks.
-
useId
useId is a new hook for generating unique IDs on both the client and server while avoiding hydration mismatches. It is primarily useful for component libraries integrating with accessibility APIs that require unique IDs. This solves an issue that already exists in React 17 and below, but it’s even more important in React 18 because of how the new streaming server renderer delivers HTML out-of-order. See docs here. -
useTransition
useTransition and startTransition let you mark some state updates as not urgent. Other state updates are considered urgent by default. React will allow urgent state updates (for example, updating a text input) to interrupt non-urgent state updates (for example, rendering a list of search results). See docs here -
useDeferredValue
useDeferredValue lets you defer re-rendering a non-urgent part of the tree. It is similar to debouncing, but has a few advantages compared to it. There is no fixed time delay, so React will attempt the deferred render right after the first render is reflected on the screen. The deferred render is interruptible and doesn’t block user input. See docs here.
I will explain these hooks with code. Not thoroughly.
I just want to give you a quick view.
If you want to know more detail, google for it and you will be able to find a lot of materials online.
Before starting it, if you use ReactDOM.render
replace it with createRoot
.
*createRoot
: New method to create a root to render or unmount. Use it instead of ReactDOM.render. New features in React 18 don’t work without it. See docs here.
I just set it up like this.
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import reportWebVitals from './reportWebVitals';
const container = document.getElementById('root') || document.body;
const root = createRoot(container);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
useId
Have you used uuid
to generate uniqueid for identifying nodes or something else before?
You can use 'useId' now.
import React, {
useState,
useCallback,
useMemo,
useRef,
useEffect,
useId,
} from 'react';
interface TimerItem {
id: string;
createdAt: Date;
tm: NodeJS.Timeout;
}
let num = 1;
let count = () => {
return num++ % 10000;
};
function Timer() {
const [timers, setTimers] = useState<TimerItem[]>([]);
const [workIn, setWorkIn] = useState(false);
const id = useId(); // generate uniqueId
const delUniqueId = useRef<string | null>(null);
const toggle = useCallback(() => setWorkIn((prev) => !prev), []);
const addTimer = useCallback(() => {
// create new timer
const itemId = `${id}${count()}`;
const newItem = {
id: itemId,
createdAt: new Date(),
tm: setTimeout(() => {
const tmInv = setInterval(() => {
if (!delUniqueId.current) {
// insert this uniqueId into delUniqueId to remove and execute worker using toggle
delUniqueId.current = itemId;
toggle();
// if delUniqueId is changed successfully, clear this timer
clearInterval(tmInv);
}
}, 50);
}, 2000),
};
setTimers((prevTimers) => [...prevTimers, newItem]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!delUniqueId.current) return;
// remove a timer by delUniqueId
setTimers(timers.filter((t) => t.id !== delUniqueId.current));
delUniqueId.current = null;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [workIn]);
const children = useMemo<React.ReactNode>(() => {
return (
<>
{timers.map((timer) => (
<div key={timer.id}>
<span>
Timer / {timer.id} / {timer.createdAt.getMinutes()}::
{timer.createdAt.getSeconds()}
</span>
</div>
))}
</>
);
}, [timers]);
return (
<div>
<button onClick={addTimer}>Add Timer</button>
<hr />
{children}
</div>
);
}
function App() {
return (
<>
<Timer />
<Timer />
<Timer />
</>
);
}
It renders three Timer
. each timer component has uniqueid. You can identify with the id whose the data.
Did you see :r1:, :r3:, :r5:?
Yes, I'm not sure if it's a good example.
Anyway, you can use useId
to generate uniqueid.
But, please note that
useId is not for generating keys in a list. Keys should be generated from your data.* See docs
useTransition, startTransition
some state updates as not urgent and other state updates are considered urgent by default?
Use startTransition
for non-urgent state updates.
import React, {
useEffect,
useState,
} from 'react';
const nodes: React.ReactNode[] = [];
for (let i = 1; i <= 5000; i++) {
nodes.push(<div>{Math.random() * i}</div>);
}
function App() {
const [text, setText] = useState('');
const [random, setRandom] = useState<React.ReactNode[]>([]);
useEffect(() => {
if (!text) return;
setRandom(nodes);
}, [text]);
return (
<>
<input
type="text"
onChange={(e) => setText(e.target.value)}
value={text}
/>
<>{random}</>
</>
);
}
Here is an example.
As you see, it almost stopped typing when I type.
If you consider other components rendering(below random number list) is not urgent, you can use 'startTransition' like this.
import React, { useEffect, useState, startTransition } from 'react';
const nodes: React.ReactNode[] = [];
for (let i = 1; i <= 5000; i++) {
nodes.push(<div>{Math.random() * i}</div>);
}
function App() {
const [text, setText] = useState('');
const [random, setRandom] = useState<React.ReactNode[]>([]);
useEffect(() => {
if (!text) return;
startTransition(() => {
setRandom(nodes);
});
}, [text]);
return (
<>
<input
type="text"
onChange={(e) => setText(e.target.value)}
value={text}
/>
<>{random}</>
</>
);
}
Although there is a bit stop(other components have to render anyway), it was certainly better than before.
If you need a loading something, you can use useTransition
import React, { useEffect, useState, useTransition } from 'react';
const nodes: React.ReactNode[] = [];
for (let i = 1; i <= 5000; i++) {
nodes.push(<div>{Math.random() * i}</div>);
}
function App() {
const [text, setText] = useState('');
const [random, setRandom] = useState<React.ReactNode[]>([]);
const [isPending, startTransition] = useTransition();
useEffect(() => {
if (!text) return;
startTransition(() => {
setRandom(nodes);
});
}, [text]);
return (
<>
<input
type="text"
onChange={(e) => setText(e.target.value)}
value={text}
/>
{isPending ? 'loading...' : <>{random}</>}
</>
);
}
useDeferredValue
Something's change affects other renderings?
But you have to render something's change first and it's okay that the other follows behind it?
Use useDeferredValue
.
import React, { useState, useMemo } from 'react';
function App() {
const [text, setText] = useState('');
const random = useMemo<React.ReactNode>(() => {
const children: React.ReactNode[] = [];
for (let i = 1; i <= 3000; i++) {
children.push(<div>{Math.random() * i}</div>);
}
return children;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [text]);
return (
<>
<input
type="text"
onChange={(e) => setText(e.target.value)}
value={text}
/>
<>{random}</>
</>
);
}
Here is an example.
It renders 3000 random nodes depending on the text's change.
There is a lot of delays, right?
Let's use useDeferredValue
import React, { useDeferredValue, useState, useMemo } from 'react';
function App() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
const random = useMemo<React.ReactNode>(() => {
const children: React.ReactNode[] = [];
for (let i = 1; i <= 1000; i++) {
children.push(<div>{Math.random() * i}</div>);
}
return children;
}, [deferredText]);
return (
<>
<input
type="text"
onChange={(e) => setText(e.target.value)}
value={text}
/>
<>{random}</>
</>
);
}
We used deferredText as a dependency of the useMemo.
It's similar to debouncing.
Conclusion
React 18 New Hooks! There are other new features.
I'd recommend you google them before applying them to your project.
Make your strategies in concurrent rendering for your users.
React
has given us another power :)
Happy coding!
Top comments (2)
It's the best post I've ever seen. I am sure that he will be best frontend engineer in the near future
Thank you, bro. Let's grab a coffee next week! lol