Telerik blogs
ReactT2 Dark_1200x303

In this blog post, we won’t just describe what React hooks do, but we’ll also get our hands dirty and cover how to use them in the code.

React 18 is a major release that comes with new features, such as concurrent rendering, automated batching, transitions, new APIs and hooks. In this tutorial, we will cover the five new hooks that arrived in React 18:

  • useId
  • useDeferredValue
  • useTransition
  • useSyncExternalStore
  • useInsertionEffect

Are you feeling a bit rusty on the topic of hooks in general? Then you could check out the Guide to Learning React Hooks our React experts have prepared. And if you’re interested to learn about more popularly used hooks, check out this blog on useCallback and useRef.

However, instead of just describing what they do and how to use them, we will get our hands dirty and cover how to actually use them in the code. For that, we will create contrived state management and CSS-in-JS solutions. Let’s dive in!

React Hooks Project Code

You can find full code examples for this project in this GitHub repository. Below you can also find an interactive StackBlitz.

React Hooks Project Setup

If you would like to follow this tutorial, you can quickly scaffold a new React project using Vite by running the command below:

$ npm create vite@latest react-18-hooks --template react

After the project is scaffolded, move into it, install all libraries and start the dev server.

$ cd react-18-hooks && npm install && npm run dev

We will use Tailwind for styles, but instead of going through the whole setup process, we will take advantage of the CDN version. Just update the index.html file and add the script below.

index.html

<script src="https://cdn.tailwindcss.com"></script>

That’s it for the setup. Let’s have a look at the first new hook—useId.

If you’ve never heard of Vite before, I’ve written an article about it—What Is Vite: The Guide to Modern and Super-Fast Project Tooling.

useId

The times when React ran only on client-side are long gone. With frameworks like Next.js, Gatsby or Remix and new features like server components, React is used on both client- and server-side.

Until version 18, React did not offer any good way to generate unique IDs that could be used for server- and client-rendered content. The important thing to remember is that the HTML markup that was rendered on the client should match the one rendered on the server. Otherwise, you will be welcomed with a React server hydration mismatch error.

Here’s a situation in which it could happen: Let’s say we have a form with an input field for which we need to generate a unique id.

const Comp = props => {
  const uid = uuid()
  return (
  	<form>
    	<label htmlFor={uid}>Name</label>
<input id={uid} type="text" />
    </form>
  )
}

The component above would have ids generated once on the server, and new ones would be generated on the client-side. This would result in a mismatch in the DOM. That’s where the useId hook comes into play.

import { useId } from 'react'
const Comp = props => {
  const uid = useId()
  return (
  	<form>
    	<label htmlFor={uid}>Name</label>
<input id={uid} type="text" />
    </form>
  )
}

The useId hook can be used to generate unique IDs that will be the same on the server- and client-side and thus help to avoid the mismatch error.

If we have more fields than one, we can always use string interpolation and add a prefix or a suffix to the unique id. You can create a new file called UseIdExample.jsx with the code below.

src/examples/UseIdExample.jsx

import { useId } from "react";
 
const UseIdExample = props => {
  const uid = useId();
  return (
    <div>
      <labelhtmlFor={`${uid}-name`}>
        Name
      </label>
      <input       
        id={`${uid}-name`}
      />
      <div>
        Generated unique user input id: {`${uid}-name`}
      </div>
 
      <label htmlFor={`${uid}-age`}>
        Age
      </label>
      <input
        id={`${uid}-age`}
      />
 
      <div>Generated unique age input id: {`${uid}-age`}</div>
    </div>
  );
};
 
export default UseIdExample;

Next, update the App component to render the UseIdExample.

src/App.jsx

import "./App.css";
import UseIdExample from "./examples/UseIdExample";
 
function App() {
  return (
    <div className="App space-y-16">
      <UseIdExample />
    </div>
  );
}
 
export default App;

The image below shows what you should see. React generates a unique id with a colon as a prefix and suffix.

useId Example has inputs for name and age, with generated unique ids for each

useDeferredValue and useTransition

With the new concurrent renderer, React can interrupt and pause renders. This means that if a new high-priority render is scheduled, React can stop the current low-priority rendering process and handle the upcoming one first.

A high-priority render could be caused by a user’s click or input. React provides two new React hooks that can be used to indicate low-priority updates— useDefferedValue and useTransition. This provides a new way of optimizing React apps, as developers can now specify which state updates are low priority.

useDeferredValue

First, let’s have a look at the useDeferredValue hook. Below we have a simple feature that allows a user to search for meals.

src/examples/UseDeferredValueExample.jsx

import {
  memo,
  Suspense,
  useDeferredValue,
  useEffect,
  useRef,
  useState,
} from "react";
 
const Meals = memo(props => {
  const { query } = props;
  const abortControllerRef = useRef(null);
  const [meals, setMeals] = useState([]);
 
  const searchMeals = async query => {
    abortControllerRef.current?.abort();
    abortControllerRef.current = new AbortController();
 
    const response = await fetch(
      `https://www.themealdb.com/api/json/v1/1/search.php?s=${query}`,
      {
        signal: abortControllerRef.current.signal,
      }
    );
    const data = await response.json();
    setMeals(data.meals || []);
  };
 
  useEffect(() => {
    searchMeals(query);
    
    return () => {
      abortControllerRef.current?.abort();
    }
  }, [query]);
 
  return (
    <>
      {Array.isArray(meals) ? (
        <ul className="mt-3 space-y-2 max-h-[30rem] overflow-auto">
          {meals.map(meal => {
            const { idMeal, strMeal } = meal;
            return <li key={idMeal}>{strMeal}</li>;
          })}
        </ul>
      ) : null}
    </>
  );
});
 
const UseDeferredValueExample = props => {
  const [query, setQuery] = useState("");
  const deferredQuery = useDeferredValue(query);
 
  return (
    <div>
      <h2 className="text-xl font-bold mb-4">useDeferredValue Example</h2>
      <div>
        <div>
          <label htmlFor="mealQuery" className="mb-1 block">
            Meal
          </label>
        </div>
        <input
          id="mealQuery"
          className="shadow border border-slate-100 px-4 py-2"
          value={query}
          onChange={e => {
            setQuery(e.target.value);
          }}
        />
      </div>
      <Suspense fallback="Loading results...">
        <Meals query={deferredQuery} />
      </Suspense>
    </div>
  );
};
 
export default UseDeferredValueExample;

When a user types something into the query input, the query state is updated. However, instead of passing it directly to the Meals component, it is passed to the useDeferredValue hook instead. The hook returns a deferredQuery value, which then is passed to the Meals component. We let React decide when exactly should the deferredQuery state change to the latest query value.

Note that the Meals component is wrapped with memo to make sure it only re-renders when deferredQuery changes and not query. The useDeferredValue hook is similar to how bouncing or throttling works, but the difference is that instead of waiting until a specified amount of time has passed, React can start the work immediately when it’s done with higher priority work.

Now we need to add the UseDeferredValueExample component in the App.jsx file.

src/App.jsx

import "./App.css";
import UseIdExample from "./examples/UseIdExample";
import UseDeferredValueExample from "./examples/UseDeferredValueExample";
 
function App() {
  return (
    <div className="App space-y-16">
      <UseIdExample />
      <UseDeferredValueExample />
    </div>
  );
}
 
export default App;

The GIF below shows what the search functionality should look like.

useDeferredValue Example - Search meals functionality shows the user typing

Next, let’s have a look at the useTransition hook.

useTransition

The useTransition hook is quite similar to useDeferredValue, but we have more control over when to start a low-priority update. The useTransition hook returns a tuple with the isPending value that indicates whether a transition is currently happening and the startTransition method.

const [isPending, startTransition] = useTransition()

Let’s replace the useDeferredValue hook from our previous example and use the useTransition hook instead.

src/examples/UseTransitionExample.jsx

import {
  memo,
  Suspense,
  useTransition,
  useEffect,
  useRef,
  useState,
} from "react";
 
const Meals = memo(props => {
  const { query } = props;
  const [meals, setMeals] = useState([]);
  const abortControllerRef = useRef(null);
  const [isPending, startTransition] = useTransition();
 
  const searchMeals = async query => {
    abortControllerRef.current?.abort();
    abortControllerRef.current = new AbortController();
 
    const response = await fetch(
      `https://www.themealdb.com/api/json/v1/1/search.php?s=${query}`,
      {
        signal: abortControllerRef.current.signal,
      }
    );
    const data = await response.json();
    startTransition(() => {
      setMeals(data.meals || []);
    });
  };
 
  useEffect(() => {
    searchMeals(query);
    return () => {
      abortControllerRef.current?.abort();
    };
  }, [query]);
 
  return (
    <>
      {isPending ? <p>Loading...</p> : null}
      {Array.isArray(meals) ? (
        <ul className="mt-3 space-y-2 max-h-[30rem] overflow-auto">
          {meals.map(meal => {
            const { idMeal, strMeal } = meal;
            return <li key={idMeal}>{strMeal}</li>;
          })}
        </ul>
      ) : null}
    </>
  );
});
 
const UseTransitionExample = props => {
  const [query, setQuery] = useState("");
 
  return (
    <div>
      <h2 className="text-xl font-bold mb-4">useTransition Example</h2>
      <div>
        <div>
          <label htmlFor="mealQuery" className="mb-1 block">
            Meal
          </label>
        </div>
        <input
          id="mealQuery"
          className="shadow border border-slate-100 px-4 py-2"
          value={query}
          onChange={e => {
            setQuery(e.target.value);
          }}
        />
      </div>
      <Suspense fallback="Loading results...">
        <Meals query={query} />
      </Suspense>
    </div>
  );
};
 
export default UseTransitionExample;

Instead of having a deferred state for the query value, we wrap the setMeals update with the startTransition instead.

startTransition(() => {
  setMeals(data.meals || []);
});

If React would be in the middle of processing the setMeals update but a higher priority update, like a user click, would be scheduled, the setMeals update would be paused.

Lastly, render UseTransitionExample in the App component.

src/App.jsx

import "./App.css";
import UseIdExample from "./examples/UseIdExample";
import UseDeferredValueExample from "./examples/UseDeferredValueExample";
import UseTransitionExample from "./examples/useTransitionExample";
 
function App() {
  return (
    <div className="App space-y-16">
      <UseIdExample />
      <UseDeferredValueExample />
      <UseTransitionExample />
    </div>
  );
}
 
export default App;

useSyncExternalStore

The useSyncExternalStore is a hook that was created for state management libraries. Its purpose is to provide an ability to read and subscribe from external data sources in a way that works with concurrent rendering features like selective hydration and time slicing.

An external store needs to provide at least two arguments—subscribe and get state snapshot methods. The former allows React to subscribe to any store changes, and the latter returns the store state. Here’s a simple example of how to use the useSyncExternalStore hook.

const state = useSyncExternalStore(store.subscribe, store.getState);

The store.getState method returns the whole external state, but we can also pass a function that returns only a part of it. For instance, if the store has a field called name, we could get just the name value from the store state.

const state = useSyncExternalStore(
  store.subscribe, 
  () => store.getState().name
);

The useSyncExternalStore can also accept a third argument, which can be used to provide a state snapshot that was created if the React app was server-side rendered. We won’t be diving into it in this article, as server-side rendering comes with its own rules and setup that is out of the scope for this article.

const state = useSyncExternalStore(
  store.subscribe, 
  store.getState, 
  () => INITIAL_SERVER_SNAPSHOT
);

That’s a nice explanation so far, but how could we use it in practice? Fortunately, creating a state management library doesn’t necessarily have to be extremely complicated, and Zustand is a good example of that. Here’s a mini Zustand implementation utilizing the useSyncExternalStore hook.

First, we need store creation logic.

src/examples/createStore.js

import { useSyncExternalStore } from "react";
import produce from "immer";
 
export const createStore = createStateFn => {
  // Create a new copy of the state object
  let state = {};
 
  // Listeners Set to store all store subscribers
  const listeners = new Set();
 
  // Add a new subscriber
  const subscribe = listener => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  };
 
  const setState = updater => {
    // Store a reference to the current state for later
    const prevState = state;
    // Create a deep clone of the current state
    // so it can be easily modified
    const nextState = produce(state, updater);
    state = nextState;
    // Notify all subscribers about the state update and pass new and previous states
    listeners.forEach(listener => listener(nextState, prevState));
  };
 
  const getState = () => state;
 
  const useStore = selector => {
    // Sync the store
    return useSyncExternalStore(
      subscribe,
      typeof selector === "function" ? () => selector(getState()) : getState
    );
  };
 
  useStore.subscribe = subscribe;
 
  state = createStateFn(setState, getState);
 
  return useStore;
};

The createStore creates a new state and a few methods:

  • subscribe – adds a new listener that will be notified when the state changes
  • setState – updates the state in an immutable way by utilizing immer and notifies all subscribers
  • getState – returns current state
  • useStore – a wrapper around useSyncExternalStore that can be used to consume the store state

Now we can use the createStore method to create a new store. Below, we create a count store with methods to increment, decrement, divide and multiply the count.

src/examples/UseSyncExternalStoreExample.jsx

import { useEffect } from "react";
import { createStore } from "./createStore";
 
const useCountStore = createStore(set => {
  return {
    count: 0,
    decrement: () => {
      set(state => {
        state.count -= 1;
      });
    },
    increment: () => {
      set(state => {
        state.count += 1;
      });
    },
    divide: () => {
      set(state => {
        state.count /= 2;
      });
    },
    multiply: () => {
      set(state => {
        state.count *= 2;
      });
    },
  };
});
 
const UseSyncExternalStoreExample = props => {
  const countStore = useCountStore();
  const multipliedCount = useCountStore(store => store.count * 2);
  const multiply = useCountStore(store => store.multiply);
 
  useEffect(() => {
    const unsubscribe = useCountStore.subscribe((state, prevState) => {
      console.log("State changed");
      console.log("Prev state", prevState);
      console.log("New state", state);
    });
 
    return unsubscribe;
  }, []);
 
  return (
    <div>
      <h2 className="text-xl font-bold mb-4">useSyncExternalStore Example</h2>
 
      <div>Count: {countStore.count}</div>
      <div>Multiplied Count: {multipliedCount}</div>
      <div className="flex gap-4 mt-4">
        <button
          className="bg-sky-700 text-sky-100 px-4 py-3"
          onClick={countStore.decrement}
        >
          Decrement
        </button>
        <button
          className="bg-sky-700 text-sky-100 px-4 py-3"
          onClick={countStore.increment}
        >
          Increment
        </button>
        <button
          className="bg-sky-700 text-sky-100 px-4 py-3"
          onClick={countStore.divide}
        >
          Divide
        </button>
        <button
          className="bg-sky-700 text-sky-100 px-4 py-3"
          onClick={multiply}
        >
          Multiply
        </button>
      </div>
    </div>
  );
};
 
export default UseSyncExternalStoreExample;

Finally, add the UseSyncExternalStoreExample component in the App.jsx file.

src/App.jsx

import "./App.css";
import UseSyncExternalStoreExample from "./examples/UseSyncExternalStoreExample";
 
function App() {
  return (
    <div className="App space-y-16">
      <UseSyncExternalStoreExample />
    </div>
  );
}
 
export default App;

Here’s how our implementation looks in action.

useSyncExternalStore example shows decrement, increment, divide, multiply buttons affecting the Count and Multiplied Count.

Note that the store creation code isn’t really optimized, so if you like this approach to an using external state, just use the Zustand library.

useInsertionEffect

The useInsertionEffect should only be used by CSS-in-JS libraries to dynamically insert styles into the DOM. This hook has an identical signature to useEffect, but it runs synchronously before all DOM mutations. Thus, if you’re not injecting any CSS styles into the DOM, you shouldn’t use it.

I wondered whether I should create a practical example of how to use the useInsertionEffect since I never really looked under the hood of CSS-in-JS libraries, but here it is. A naive, contrived and totally unoptimized CSS-in-JS implementation—meaning don’t use it at home.

First, we have the useStyles hook that accepts an object with styles and props.

src/examples/useStyles.js

import { useInsertionEffect, useMemo } from "react";
import { nanoid } from "nanoid";
import styleToCss from "style-object-to-css-string";
 
export const useStyles = (stylesCreator, props = {}) => {
  /**
   * Create a styles object with classes that will be passed to elements and the styleRules which will be inserted into a  stylesheet
   */
  const [styles, styleRules] = useMemo(() => {
    // styles for the className prop
    const styles = {};
    // style rules for a stylesheet
    const styleRules = [];
    for (const [styleProperty, styleValue] of Object.entries(
      stylesCreator(props)
    )) {
      // Generate a unique hashed class name
      const hashedClassName = `${styleProperty}_${nanoid()}`;
      styles[styleProperty] = hashedClassName;
      // Create formatted rule that will be inserted into a stylesheet in the useInsertionEffect
      const rule = `.${hashedClassName} {${styleToCss(styleValue)}}`;
      styleRules.push(rule);
    }
 
    return [styles, styleRules];
  }, [stylesCreator, props]);
 
  useInsertionEffect(() => {
    /**
     * Create a new stylsheet, insert it into the DOM and add style rules
     */
    const stylesheet = document.createElement("style");
    document.head.appendChild(stylesheet);
 
    for (const rule of styleRules) {
      stylesheet.sheet.insertRule(rule);
    }
 
    return () => {
      document.head.removeChild(stylesheet);
    };
  }, [styles, styleRules]);
  return styles;
};

In the useMemo, a unique hashed class name is generated for each object style property, and the object with styles is converted to a CSS string using the style-object-to-css-string library. Each rule is pushed into the styleRules array.

// Generate a unique hashed class name
const hashedClassName = `${styleProperty}_${nanoid()}`;
styles[styleProperty] = hashedClassName;
// Create formatted rule that will be inserted into a stylesheet in the useInsertionEffect
const rule = `.${hashedClassName} {${styleToCss(styleValue)}}`;
styleRules.push(rule);

In the useInsertionEffect, we create a new style element, loop through the style rules and insert each of them into the stylesheet.

const stylesheet = document.createElement("style");
document.head.appendChild(stylesheet);
 
for (const rule of styleRules) {
  stylesheet.sheet.insertRule(rule);
}

Last but not least, the stylesheet is removed from the DOM in the cleanup function.

return () => {
  document.head.removeChild(stylesheet);
};

Now we can use our useStyles hook, so create a new file called UseInsertionEffectExample.jsx and copy the code below into it.

src/examples/UseInsertionEffectExample.jsx

import { useState } from "react";
import { useStyles } from "./useStyles";
 
const styles = props => {
  return {
    buttonsContainer: {
      display: "flex",
      flexDirection: "column",
      gap: "1rem",
    },
    button: {
      backgroundColor: "#9333ea",
      color: "#faf5ff",
      fontSize: "18px",
      padding: "8px 12px",
      width: `${props.width}px`,
    },
  };
};
 
const UseInsertionEffectExample = props => {
  const [width, setWidth] = useState(150);
  const style = useStyles(styles, { width });
  return (
    <div>
      <h2 className="text-xl font-bold mb-4">useInsertionEffect Example</h2>
      <div>
        <div className={style.buttonsContainer}>
          <button
            className={style.button}
            onClick={() => setWidth(width => width - 5)}
          >
            Decrement
          </button>
          <button
            className={style.button}
            onClick={() => setWidth(width => width + 5)}
          >
            Increment
          </button>
        </div>
      </div>
    </div>
  );
};
 
export default UseInsertionEffectExample;

We have the styles object with styles for the buttons that are then passed to the useStyles hook. The useStyles hook returns an object with classes that look something like this:

{
    "buttonsContainer": "buttonsContainer_slsoSY55FjCzpn0fbFdjA",
    "button": "button_YfGnVuvh3ESCaIZdEVwNr"
}

The buttonsContainer class is passed to a div element, while the button class to Decrement and Increment buttons.

The width state changes every time one of the buttons is clicked. When that happens, the old styles are removed, and new ones are created and inserted into the DOM again inside of the useInsertionEffect.

Next, we need to render the UseInsertionEffectExample component.

src/App.jsx

import "./App.css";
import UseInsertionEffectExample from "./examples/UseInsertionEffectExample";
 
function App() {
  return (
    <div className="App space-y-16">
      <UseInsertionEffectExample />
    </div>
  );
}
 
export default App;

The GIF below shows what the UseInsertionEffectExample should look like.

useInsertionEffect example showing decrement and increment buttons changing the button widths

Summary

We have covered the new hooks that were added in React 18. Interestingly enough, you might actually find yourself not using any of them.

useSyncExternalStore and useInsertionEffect are specifically designed for library authors who work on state management and CSS-in-JS solutions. The useId hook is useful if your React app runs on both client and server, so if your React app runs only on the client-side, you won’t really need it.

Furthermore, useTransition and useDeferredValue can be used to mark some state updates as less important and defer, but this isn’t something that all applications will necessarily need.

Nevertheless, all of these hooks are a great addition, and I can’t wait to see what else React will bring in the future. If you would like to read more about React 18, make sure to read the official blog page that covers new features.


Thomas Findlay-2
About the Author

Thomas Findlay

Thomas Findlay is a 5-star rated mentor, full-stack developer, consultant, technical writer and the author of “React - The Road To Enterprise” and “Vue - The Road To Enterprise.” He works with many different technologies such as JavaScript, Vue, React, React Native, Node.js, Python, PHP and more. Thomas has worked with developers and teams from beginner to advanced and helped them build and scale their applications and products. Check out his Codementor page, and you can also find him on Twitter.

Related Posts

Comments

Comments are disabled in preview mode.