So far, we have discussed five different hooks in React:
useState
: For handling states.useReducer
: For handling more complex state logic.useContext
: For accessing and sharing data across components without prop drilling.useRef
: For directly interacting with DOM elements.useEffect
: For handling side effects, such as data fetching or updating the DOM after rendering.
Creating your custom hook
Besides these built-in hooks, sometimes you may need a hook for more specific purposes, such as fetching data, tracking window sizes, connecting to WebSocket servers, and more. Instead of repeating the same logic over and over again, React allows you to create custom hooks.
Let's go back to one of our previous examples:
1import { useState, useEffect } from "react";
2
3export default function App() {
4 const [user, setUser] = useState([]);
5 const [loading, setLoading] = useState(true);
6
7 useEffect(() => {
8 async function fetchUser() {
9 const response = await fetch("https://randomuser.me/api/");
10 const data = await response.json();
11 setUser(data.results[0]);
12 setLoading(false);
13 }
14
15 fetchUser();
16 }, []);
17
18 if (loading) return <div>Loading user...</div>;
19
20 return (
21 <div>
22 <h1>
23 {user.name.first} {user.name.last}
24 </h1>
25 <p>Email: {user.email}</p>
26 <img src={user.picture.large} alt="User profile" />
27 </div>
28 );
29}
In this example, the user information is fetched from an API and used to update the user profile. However, what if your app needs to retrieve additional data from multiple APIs? In this case, you'll need a more flexible approach that allows your app to access data from different sources.
To achieve this, you can extract the data retrieval portion of our code, and create a custom hook like this:
libs/hooks.js
1import { useState, useEffect } from "react";
2
3function useFetchData(url) {
4 const [data, setData] = useState(null);
5 const [loading, setLoading] = useState(true);
6
7 useEffect(() => {
8 async function fetchData() {
9 const response = await fetch(url);
10
11 setData(await response.json());
12 setLoading(false);
13 }
14
15 fetchData();
16 }, [url]); // Add url as dependency
17
18 return { data, loading };
19}
20
21export { useFetchData };
First of all, even though this is a custom hook and technically could be named however you want, it should still follow the naming convention and start with the keyword use
. We also updated user
to data
because this hook is designed to fetch general data from an API, not just user-specific data.
Additionally, the API URL should be passed to the hook as a parameter (url
), rather than being hardcoded. Make sure to include the url
as a dependency for the effect, so the hook fetches data whenever the url changes.
Lastly, this hook will return the retrieved data
and the loading
state, and don't forget export this useFetchData()
hook at the end.
To use this custom hook, simply import the useFetchData()
like this:
1import { useFetchData } from "./libs/hooks";
2
3export default function App() {
4 const { data, loading } = useFetchData("https://randomuser.me/api/");
5 const user = data?.results[0]; // Add optional chaining
6
7 if (loading) return <div>Loading user...</div>;
8
9 return <div>. . .</div>;
10}
Line 4, here we pass the API URL, and get the retrieved data
and loading
state.
Line 5, depending on the use case, the retrieved data might need to be processed first. In this case, remember to use the optional chaining (?
) to safely access properties. Because data
will be initialized as null
, and trying to access it's property directly will cause an error.
Other React hooks
In addition to the hooks we've discussed in previous lesson, there are several other built-in hooks available. While these may not be as commonly used, they can be quite useful in specific scenarios.
useCallback
: Used to memoize a function, preventing it from being re-created on every render. This is particularly useful when passing callbacks to child components, as it helps to avoid unnecessary re-renders by ensuring the function reference remains stable unless its dependencies change.
1import { useCallback, useState } from "react";
2
3function ParentComponent() {
4 const [count, setCount] = useState(0);
5
6 const increment = useCallback(() => {
7 setCount((prev) => prev + 1);
8 }, []);
9
10 return <ChildComponent increment={increment} />;
11}
12
13function ChildComponent({ increment }) {
14 return <button onClick={increment}>Increment</button>;
15}
useId
: Generates a unique ID, which is stable across renders. This is useful for accessibility purposes or when you need unique identifiers for elements.
1import { useId } from "react";
2
3function Form() {
4 const id = useId();
5
6 return (
7 <div>
8 <label htmlFor={id}>Name:</label>
9 <input id={id} type="text" />
10 </div>
11 );
12}
useMemo
: Used to memoize expensive calculations or derived values, preventing them from being recalculated on every render unless their dependencies change. UseuseMemo
when you have expensive computations or derived values that don’t need to be recalculated every time the component renders.
1import { useMemo, useState } from "react";
2
3function ExpensiveCalculationComponent() {
4 const [count, setCount] = useState(0);
5
6 const expensiveCalculation = useMemo(() => {
7 console.log("Expensive calculation running");
8 return count * 2;
9 }, [count]);
10
11 return (
12 <div>
13 <p>Result: {expensiveCalculation}</p>
14 <button onClick={() => setCount(count + 1)}>Increment</button>
15 </div>
16 );
17}
useSyncExternalStore
: Used to subscribe to external stores (like global state management libraries) in a way that is concurrent-mode safe. It ensures the state is read synchronously while also providing external subscription management.
1import { useSyncExternalStore } from "react";
2
3function useMyStore() {
4 return useSyncExternalStore(
5 (listener) => {
6 // Subscribe to the store
7 myStore.subscribe(listener);
8 return () => myStore.unsubscribe(listener);
9 },
10 () => myStore.getState() // Get the current state
11 );
12}
13
14function MyComponent() {
15 const state = useMyStore();
16 return <div>{state}</div>;
17}
useTransition
: Allows you to mark a state update as non-urgent, enabling smoother transitions between UI states by prioritizing urgent updates over non-urgent ones. UseuseTransition
when you want to prioritize rendering and make certain state updates non-urgent, particularly in scenarios where you want to maintain responsiveness during complex state transitions.
1import { useState, useTransition } from "react";
2
3function MyComponent() {
4 const [isPending, startTransition] = useTransition();
5 const [data, setData] = useState(null);
6
7 const fetchData = () => {
8 startTransition(() => {
9 // Simulate data fetch
10 setData("Fetched data");
11 });
12 };
13
14 return (
15 <div>
16 <button onClick={fetchData}>Fetch Data</button>
17 {isPending ? <p>Loading...</p> : <p>{data}</p>}
18 </div>
19 );
20}
useDeferredValue
: Allows you to defer updating a value to a non-urgent priority, improving responsiveness by delaying less important updates. UseuseDeferredValue
when you want to prevent UI sluggishness by deferring less critical updates, such as search results while a user is typing.
1import { useDeferredValue, useState } from "react";
2
3function SearchComponent() {
4 const [searchQuery, setSearchQuery] = useState("");
5 const deferredSearchQuery = useDeferredValue(searchQuery);
6
7 return (
8 <div>
9 <input
10 type="text"
11 value={searchQuery}
12 onChange={(e) => setSearchQuery(e.target.value)}
13 />
14 <SearchResults query={deferredSearchQuery} />
15 </div>
16 );
17}
18
19function SearchResults({ query }) {
20 return <div>Searching for: {query}</div>;
21}
useImperativeHandle
: Allows you to customize the instance value (or "ref") that gets exposed when usingref
in a parent component. It is mainly used to expose specific methods to the parent component via a ref. UseuseImperativeHandle
when you need to expose custom methods or properties from a child component to its parent, typically when using refs.
1import { useImperativeHandle, useRef, forwardRef } from "react";
2
3const ChildComponent = forwardRef((props, ref) => {
4 useImperativeHandle(ref, () => ({
5 customMethod() {
6 console.log("Custom method called");
7 },
8 }));
9
10 return <div>Child Component</div>;
11});
12
13function ParentComponent() {
14 const childRef = useRef();
15
16 return (
17 <div>
18 <ChildComponent ref={childRef} />
19 <button onClick={() => childRef.current.customMethod()}>
20 Call Custom Method
21 </button>
22 </div>
23 );
24}
useInsertionEffect
: Used to insert styles or effects in a way that avoids the "flash of unstyled content" (FOUC). It runs before layout effects and after rendering to ensure the styles are applied at the right moment. UseuseInsertionEffect
when you need to inject or manage styles or perform critical DOM manipulations that must occur before layout effects.
1import { useInsertionEffect } from "react";
2
3function MyStyledComponent() {
4 useInsertionEffect(() => {
5 const style = document.createElement("style");
6 style.innerHTML = `.my-class { color: red; }`;
7 document.head.appendChild(style);
8
9 return () => {
10 document.head.removeChild(style);
11 };
12 }, []);
13
14 return <div className="my-class">Styled Text</div>;
15}
useLayoutEffect
: Similar touseEffect
, but it fires synchronously after all DOM mutations and before the browser repaints. This makes it ideal for measurements or synchronous DOM updates that should happen before the user sees the layout. UseuseLayoutEffect
when you need to perform actions that must run synchronously after DOM updates, such as measuring elements or performing layout adjustments.
1import { useLayoutEffect, useRef } from "react";
2
3function MyComponent() {
4 const divRef = useRef();
5
6 useLayoutEffect(() => {
7 divRef.current.style.width = `${window.innerWidth / 2}px`;
8 });
9
10 return <div ref={divRef}>Resize Me</div>;
11}
useSyncExternalStore
: Used for reading and subscribing to external stores (like Redux) in a way that is compatible with React's concurrent rendering capabilities. UseuseSyncExternalStore
when you need to subscribe to global or external state stores, like Redux or custom store libraries.
1import { useSyncExternalStore } from "react";
2
3function useStore() {
4 return useSyncExternalStore(
5 (subscribe) => store.subscribe(subscribe),
6 () => store.getSnapshot()
7 );
8}
9
10function MyComponent() {
11 const state = useStore();
12 return <div>{state.someValue}</div>;
13}