Since the release of React 16.8 in 2019, building a reusable piece of UI (components) has become super easy. Thanks to the introduction of React Hooks. Now we don't write verbose class-based components anymore. Over the years, the React team has introduced a ton of built-in hooks that solve specific problems.
But most of the people only stick with just useState()
and useEffect()
. But there are a lot of hooks that can improve your understanding and ability to write efficient code. As an experienced developer of React, I have tried to explain each of the advanced & newly introduced hooks with proper real-world code examples. Let's build the best app together.
1. useActionState
Whenever you submit a form in React, you constantly juggle from to write the load of useStates
and useEffects
with fetch
.
With the introduction of React Server components, the useActionState
made the process much easier.
This hook requires two parameters. One is an action parameter, and the other is the initialState. It returns the updated state
, formAction
, and isPending
status. let's break them down:
const [state, formAction, isPending] = useActionState(myServerAction, initialState);
Example:
Let's say you want to build a form that sends a post request about a value in the database. You prefer to use the latest React RSC-based architecture (or you may use NextJS). Your form will catch real-time errors and send data to the DB.
Here is the whole code example:
import { useActionState } from "react";
// A server action
// NOTE: prevState is required for the useActionState hook
async function sendName(prevState: any, formData: FormData) {
const name = formData.get("name");
if (!name) {
return { error: "Name is required!" };
}
// Imagine saving to database here
return saveTODatabase(name);
}
//component
export default function ContactForm() {
const initialState = { error: "" };
const [state, formAction, isPending] = useActionState(sendName, initialState);
return (
<form action={formAction}>
<input name="name" placeholder="Enter your name" disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? "Sending..." : "Submit"}
</button>
<p className="text-red-600">{state.error && state.message}</p>
</form>
);
}
Let's break this down:
- The
useActionState()
hook got two parameters- A server action
sendName()
function that saves the name if the name input is typed. - an
initialState
object that holds the error data.
- A server action
- The
useActionState()
hook returns- The updated
state
that contains the updated error data. - The
formAction
function is passed to the form to perform this action on submit. - The
isPending
boolean to get to know the form's asynchronous status. These returned values together give flexibility to write efficient code and perform secure transactions through components and databases.
- The updated
2. useFormStatus
Another useful hook to get the proper information about a form's status. This hook returns 4 key pieces of information about a form. These are,
-
pending
(boolean). -
method
(form submission method i.e., POST, PUT) -
data
(the submitted form data) -
action
(The server action function)
// nested inside a form component
const { data, action, method, pending } = useFormStatus();
NOTE: useFormStatus()
must be used inside a component that is a direct or indirect child of a
Example:
The pending
is the key property of this hook. It checks the form asynchronous status. Moreover, it is sometimes used with useActionState()
hook that I discussed earlier.
You want to build a form that gives a good-looking loading UI needs to get the submitted data to show the client
// Submit button using useFormStatus
function SubmitButton() {
const { pending, data } = useFormStatus();
return (
<div className="mt-2">
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
{pending && (
<div className="text-sm mt-1 text-gray-500">
<p>Submitting email: <strong>{data?.get('email') as string}</strong>p>
</div>
)}
</div>
);
}
export default function AdvancedForm() {
const [state, formAction, isPending] = useActionState(handleSubmit, { message: '' });
return (
<form action={formAction} method="POST" className="max-w-sm space-y-3">
<input
type="email"
name="email"
placeholder="Enter your email"
disabled={isPending}
/>
<SubmitButton />
{state.message && <p className="text-green-600 mt-2">{state.message}</p>}
</form>
);
}
Check that the SubmitButton
component is inside a form containing component. Now the useFormStatus()
will work seamlessly accompanied by useActionState()
of the parent component.
3. useDeferredValue
It is the hook related to the performance boost. It defers the update of a value, making the UI more responsive. This is handy when updating the state with big data, heavy rendering, or slow components. It can be analogous to debouncing to some people. But there are some changes between them.
const deferredInput = useDeferredValue(input);
Example
You want to build a search component that filters through a huge amount of data, and whenever you search for something through the input, the big filtering comes as a bottleneck. It makes the UI unresponsive and makes the typing input slow.
One way to fix this is to use debouncing. But React has a built-in hook useDefferedValue()
to solve this.
import { useDeferredValue, useState } from 'react';
// bigList is a huge list of data!
function SearchComponent({ bigList }) {
const [input, setInput] = useState('');
const deferredInput = useDeferredValue(input); // defer heavy filtering
const filteredList = bigList.filter(item =>
item.includes(deferredInput)
);
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Search..."
/>
<ul>
{filteredList.map((item, i) => <li key={i}>{item}</li>)}
</ul>
</div>
);
}
Now you can see the UI is responsive, as the input gets deferred inside the filteredList
function. It tells React that this deferred input has less priority than the real input. So the real input changes immediately.
4. useTransition
This is another performance hook and is often confused with the useDeferredValue hook. This hook provides a function that tells React that these codes are less prioritized tasks. In other words, this hook manages the state transitions of states that are non-urgent, like background updates or heavy sorting/filtering. This helps to make the UI responsive.
Example:
You want to create a search component that filters a large dataset based on the input value provided in an input field. The data comes from another source. Use the useTransition()
hook to make the filtering non-urgent. This will make the input more responsive
import { useState, useTransition } from "react";
import { data } from "./data";
export default function SearchComponent() {
const [items, setItems] = useState(data);
const [input, setInput] = useState("");
const [isPending, startTransition] = useTransition();
function handleInput(e) {
//this state change is most prioritized
setInput(e.target.value);
startTransition(() => {
//this tasks are non-urgent
const filtered = data.filter((item) => {
return item.name.toLowerCase().includes(e.target.value);
});
setItems(filtered);
});
}
return (
<div>
<input type="text" value={input} onChange={handleInput} name="" id="" />
{isPending && <h1>Loading...</h1>}
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
5. useDebugValue
This hook is not that much used. Just to let you know that this type of hook exists in React :). This hook is primarily used for custom hooks to help developers debug hook values. A state passed to this hook can be seen through the DevTools.
Example
Imagine you are building a chat app and currently building a custom hook that checks whether the user is online or offline.
To debug, you can use the useDebugValue hook to check the status through the devtools
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useDebugValue(isOnline ? "Online" : "Offline"); // 👈 helpful in DevTools
//...other code
return isOnline;
}
6. useOptimistic
A latest and one of the most important hooks is the useOptimistic()
hook. This hook lets you optimistically update the UI; that is, this hook will immediately reflect the necessary UI change before the server confirms. It is especially useful for form submissions or actions that take time (like API calls).
// useOptimistic invoking
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(prevComments, newComment) => [...prevComments, { ...newComment, optimistic: true }]
);
// to call this we use
addOptimistic({comment:'A new comment added'})
useOptimistic(initialState, updater) returns a [state, update function]. When addOptimisticComment is called:
UI updates immediately using the updater function.
When the server responds, the real data gets updated in place of the optimistically updated data.
Example
You are building a dashboard for an e-commerce site. A huge amount of product data is in front of you, and you are implementing a delete function that will delete a certain product from the list of all products. Use the useOptimistic
hook to reflect the data change immediately and then delete the data from the server.
const AllProducts = ({ products }: { products: Product[] }) => {
const [optimisticProducts, setOptimisticProducts] = useOptimistic(
products,
(prevstate, val) => {
return prevstate.filter((product) => product.id !== val);
}
);
const deleteProduct = async (id: number) => {
setOptimisticProducts(id);
await removeProduct(id);
};
return (
<div>
{optimisticProducts.map((product) => {
return (
<div
/// product details code...
<button onclick={()=>deleteProduct(product.id)} className="px-3 py-2 bg-red-500 w-full hover:bg-red-600 cursor-pointer text-white rounded-md">
Delete
</button>
</div>
);
})}
</div>
);
};
7. The use API
The use
sounds like a hook, acts like a hook, but it is not a hook in React. The React documentation more specifically calls it an API.
There are some challenges while using React's hooks, such as,
- Hooks cannot be used inside conditional statements or for loops.
- Some hooks make the code more verbose.
function MessageComponent({ messagePromise }) {
const message = use(messagePromise);
const theme = use(ThemeContext);
// ...
// similar to
const message = await messagePromise
const theme = useContext(ThemeContext)
use()
solves these issues. The use
API returns the value that was read from the resource, like the resolved value of a Promise or context. For instance, you can use the load of useEffect
and useState
to fetch data, but the use
makes it just two or three lines of code. Moreover, use
works exactly the same work of useContext
hook with more flexibility.
example
You are creating a component that shows all the user's data. Based on the client's preference, you also want to change the current page's theme.
Both can be done using the use
API.
import { use } from 'react';
import { ThemeContext } from './context/themecontext'
// Simulate fetching user data
async function getUser() {
const res = await fetch('https://jsonplaceholder.typicode.com/users/1');
return res.json();
}
export default function UserPage() {
// This will suspend rendering until the data is ready
const user = use(getUser());
const theme = use(ThemeContext)
return (
<div className={theme === 'dark' ? 'dark':'light'}>
<h1>User Info</h1>
<p><strong>Name:</strong> {user.name}p>
<p><strong>Email:</strong> {user.email}p>
</div>
);
}
Conclusion
That's a wrap for part 1. I have covered some major and new hooks in this blog. Hooks are essential to create modern server components in React. I do plan to write more about some advanced hooks. Feel free to connect with me.
LinkedIn
Twitter