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:
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!
You can find full code examples for this project in this GitHub repository. Below you can also find an interactive StackBlitz.
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.
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.
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.
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.
Next, let’s have a look at the useTransition
hook.
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;
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 changessetState
– updates the state in an immutable way by utilizing immer and notifies all subscribersgetState
– returns current stateuseStore
– a wrapper around useSyncExternalStore
that can be used to consume the store stateNow 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.
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.
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.
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 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.