DEV Community

SeongKuk Han
SeongKuk Han

Posted on

React 18 New Hooks for Concurrency!

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();
Enter fullscreen mode Exit fullscreen mode

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 />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

useid example

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}</>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

not using starttransition

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}</>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

start Transition

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}</>}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

useTransition

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}</>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Not using useDeferredValue

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}</>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

useDeferredValue

consolelog value and deferredvalue

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!

Discussion (0)