React Hooks are powerful features introduced in React 16.8 that let you "Hook into" React state and lifecycle features from function components.
By eliminating the need for class-based components, Hooks have brought in a new era of declarative and composable React development. They empower you to build more concise, reusable, and testable components, enhancing overall code quality and developer experience.
In this blog post, you’ll discover different types of Hooks and their benefits and drawbacks, and the code examples provided will help you understand how to use them.
#Rules of React Hooks
React Hooks come with two rules that must be followed to ensure their proper functioning and avoid errors:
- Call Hooks at the top level: Call Hooks outside loops, conditions, or nested functions.
- Call Hooks from Reactfunctions: Only use Hooks within React function components or custom Hooks.
These rules ensure that the order of Hook calls remain consistent across renders, preventing React from getting confused about which state corresponds to which Hook.
Let's clone this Hygraph project to set up the environment for discussing the practical applications of some common Hooks. This will serve as our playground for demonstrating these Hooks in action. Now, let's get started with the useState
Hook.
#The useState Hook
This React Hook allows you to add state to functional components. It provides a way to store and manage values that can change over time within your component, triggering re-renders whenever the state is updated.
Example
function Counter() {const [count, setCount] = useState(0); // Initial state is 0return (<div><p>You clicked {count} times</p><button onClick={() => setCount(count + 1)}>Click me</button></div>);}
In the code above, the useState
Hook creates an array with the state value (count
) and a function to update the state (setCount
). The state has an initial value of 0
, but it could also be null
, an empty array ([]
), an object, or any value that suits your needs.
The useState Hook is best suited for managing state values and provides an intuitive way to manage states in functional components. If you call the setCount
function with a new value, your state will be updated, and React will automatically re-render the component with the updated state, ensuring that your UI is always up-to-date. Let's discuss the useEffect
Hook next.
#The useEffect Hook
The useEffect
Hook is a powerful React Hook that allows functional components to perform side effects. Side effects are operations that interact with the outside world or have an impact beyond the component's scope, such as fetching data from an API, directly updating the DOM, setting up subscriptions, and so on. Here's a quick implementation example:
function App() {const [count, setCount] = useState(0);useEffect(() => {document.title = `You clicked ${count} times`;}, [count]); // Dependency arrayreturn (<div><p>You clicked {count} times</p><button onClick={() => setCount(count + 1)}>Click me</button></div>);}
The useEffect takes two arguments:
- A function that contains the side effect logic (in this case, updating the document title)
- An optional dependency array (in this case,
[count]
)
The side effect function is called whenever the component mounts, whether the dependency array is present or not. It uses the current value of the count variable to update the document title.
function App() {const [count, setCount] = useState(0);useEffect(() => {document.title = `You clicked ${count} times`;// Cleanup functionreturn () => {document.title = 'Default Title';};}, [count]); // Dependency array//Component return() here}
The cleanup function (returned from the side effect function) is called when the component is about to be unmounted or when a new side effect is about to be executed. In this case, it resets the "React App" document title.
The dependencies array [] instructs React to execute the useEffect
side effect function only when the values inside it change between renders, in our case [count]
. If no dependency array is provided, the effect will run after every render, whether the values changed or not.
For a more practical implementation of the useEffect
and useState
Hooks, let's optimize the Hygraph Next.js project you cloned earlier. Update the product/[slug]/page.jsx
with the following snippets:
'use client';import { useState, useEffect } from 'react';import { GraphQLClient } from 'graphql-request';import Link from 'next/link';const hygraph = new GraphQLClient('https://api-eu-central-1.hygraph.com/v2/ck8sn5tnf01gc01z89dbc7s0o/master');const QUERY = `query ProductPageQuery($slug: String!) {product(where: { slug: $slug }) {namedescriptionprice}}`;export default function Product({ params }) {const [product, setProduct] = useState(null);const [isLoading, setIsLoading] = useState(true);useEffect(() => {const fetchProduct = async () => {try {const { product } = await hygraph.request(QUERY, { slug: params.slug });setProduct(product);} catch (error) {console.error('Error fetching product:', error);} finally {setIsLoading(false);}};fetchProduct();}, [params.slug]); // Fetch only when slug changesreturn (<><Link href="/">Home</Link>{isLoading ? (<p>Loading...</p>) : (<><h1>{product.name}</h1><p>{product.description}</p><p>€{product.price / 100}</p></>)}</>);}
The code has been refactored to utilize React's useState
Hook for managing the product data, and a loading state (isLoading ) was also added. This enables efficient re-renders only when necessary and provides a loading indicator to improve the user experience.
Additionally, the useEffect
Hook fetches product data solely when the component mounts or when the product's slug changes, preventing redundant API calls. The code also includes error handling and query optimization for robustness and readability.
#The useReducer Hook
The useReducer
is another React Hook used for state management in functional components. It's an alternative Hook like useState
when dealing with complex state management. In cases where your state logic involves multiple interconnected values or when the next state is determined by both the previous state and a specific action, useReducer
excels.
This allows for a more structured and centralized approach to state management, encapsulating complex logic within a reducer function.
How it works:
The reducer()
takes the current state and an action object as arguments. The action object has a type property (and can also have a payload). Based on the action.type
, the reducer determines how to update the state and returns the new state.
The useReducer
Hook is then called with the reducer function and an initial state. Afterwards, it returns an array with two values:
state
: This is the current state managed by the reducer.dispatch
: This function triggers state updates by sending action objects to the reducer.
To update the state, you only need to call the dispatch
function with an action
object. The reducer will then listen to the action and update the state accordingly, causing React to re-render the corresponding component.
Example
In the example, we will optimize fetching all products with the useReducer
and useEffect
Hooks. Still in our Hygraph project, update the app/page.jsx
with the following code snippets:
"use client"import { useReducer, useEffect } from 'react';import { GraphQLClient, gql } from 'graphql-request';import Link from 'next/link';const hygraph = new GraphQLClient('https://api-eu-central-1.hygraph.com/v2/ck8sn5tnf01gc01z89dbc7s0o/master');const QUERY = `{products {slugnameid}}`;const initialState = {products: [],loading: true,error: null};function reducer(state, action) {switch (action.type) {case 'FETCH_SUCCESS':return { products: action.payload, loading: false, error: null };case 'FETCH_ERROR':return { products: [], loading: false, error: action.payload };default:return state;}}export function generateMetadata() {return { title: 'Products' };}export default function Page() {const [state, dispatch] = useReducer(reducer, initialState);useEffect(() => {const fetchProducts = async () => {try {const { products } = await hygraph.request(QUERY);dispatch({ type: 'FETCH_SUCCESS', payload: products });} catch (error) {dispatch({ type: 'FETCH_ERROR', payload: error.message });}};fetchProducts();}, []);return (<div><h1>Products</h1>{state.loading ? (<p>Loading...</p>) : state.error ? (<p>Error: {state.error}</p>) : (<ul>{state.products.map(({ slug, name, id }) => (<li key={id}><Link href={`/products/${slug}`}>{name}</Link></li>))}</ul>)}</div>);}
By incorporating useReducer
, the code becomes more organized, the state management is centralized, and error handling and loading states are explicitly managed.
Another example where useReducer
truly shines is when implementing a shopping cart component. You can employ the useReducer
Hook to manage adding, removing, and modifying items within the cart. This approach helps maintain a clean and efficient state structure; however, useState
might be a better alternative for simple state updates. Next, let's discuss the useCallback
Hook.
#The useCallback Hook
The useCallback
Hook memoizes a callback function to ensure that it is not recreated on every render unless its dependencies change. This capability can be crucial for improving the performance of React applications, especially when dealing with complex components and frequent updates.
Here's how it works:
- It takes two arguments, a callback function and an array of dependencies, similar to the
useEffect
Hook. - On the initial render,
useCallback
returns the callback function you passed. - On subsequent renders,
useCallback
compares the dependencies with the previous render's dependencies. - If none of the dependencies have changed, the
useCallback
Hook will return the same function. - If any dependency has changed,
useCallback
returns a new memoized version of the callback function.
Here's an example that demonstrates how the useCallback
Hook can be leveraged to optimize further the imaginary counter
component we had in our initial examples:
import { useState, useCallback } from 'react';const Counter = () => {const [count, setCount] = useState(0);const incrementCounter = useCallback(() => {setCount(count + 1);}, [count]);const decrementCounter = useCallback(() => {setCount(count - 1);}, [count]);return (<div>Count: {count}<button onClick={incrementCounter}>Increment</button><button onClick={decrementCounter}>Decrement</button></div>);};
In this example, we define two callback functions, incrementCounter
and decrementCounter
, using the useCallback
Hook. We pass [count]
as the dependency array so that the callbacks will only be recreated if the count
value changes.
This way (memoizing the callbacks with the useCallback
Hook), we ensure that the callbacks are not unnecessarily recreated on every render, which could lead to unnecessary re-renders of child components that rely on these callbacks. Here are some primary use cases for the useCallback
Hook:
- They optimize performance by preventing unnecessary re-renders of child components that depend on callback functions.
- They ensure reference equality for callback functions passed as dependencies to other Hooks or memoized components.
- They help to avoid recreating expensive functions on every render, especially when passed as props to child components.
Like useCallback
, the useMemo
Hook also helps optimize React applications by caching the results of complex calculations. Let's discuss it in detail.
#The useMemo Hook
The useMemo Hook is another performance optimization Hook in React. It's designed to memoize the result of a calculation or an expensive React function to prevent it from being recomputed on every render of your component. Like the useCallback
Hook, it takes two arguments - a function that performs the computation and an array of dependencies.
Here's how it works:
- It only recomputes the memoized value when one of its dependencies changes.
- It returns the memoized value, which remains the same between re-renders unless the dependencies change.
- It is primarily used for performance optimization, not as a semantic guarantee. React can "forget" some previously memoized values in situations like freeing up memory for offscreen components, etc.
Here's an improved version of our counter
component that uses the useMemo
Hook to enhance its performance.
import { useState, useMemo } from 'react';function ExpensiveCalculation({ number }) {// Simulating an expensive calculationconst calculateFactorial = (num) => {if (num === 0 || num === 1) return 1;let result = 1;for (let i = 2; i <= num; i++) {result *= i;}return result;};const factorial = useMemo(() => calculateFactorial(number), [number]);return <div>Factorial of {number} is {factorial}</div>;}function Counter() {const [count, setCount] = useState(0);const [number, setNumber] = useState(5);return (<div><h2>Counter: {count}</h2><button onClick={() => setCount(count + 1)}>Increment Counter</button><h2>Factorial Calculator</h2><inputtype="number"value={number}onChange={(e) => setNumber(parseInt(e.target.value))}/><ExpensiveCalculation number={number} /></div>);}export default Counter;
In this example, the result of the ExpensiveCalculation
component was memoized. The real benefit of using useMemo
in this scenario is that the factorial calculation is only performed when the number
changes. As a result, incrementing the counter doesn't trigger a recalculation of the factorial.
When used correctly, the useMemo
Hook can significantly improve the speed and responsiveness of your applications. However, it can also negatively impact your app. Here are some notable considerations.
- In strict mode, React calls the calculation function twice to help identify accidental impurities.
- Overuse or unnecessary memoization can hurt performance due to difficulty maintaining the cache. Moving on, let's explore the
useContext
Hook as an alternative approach to managing state in React applications.
#The useContext Hook
Data in React components flow in one direction, from parent to child, through props. The useContext
Hook in React lets you access data stored in a central location, called a context, without needing to manually pass props down at every level. This is useful for managing information that needs to be available across different parts of your application, even those not directly connected in the usual parent-child structure.
The useContext
Hook itself doesn't provide mechanisms for updating the state. It only allows you to read a context's current value. The actual state updates are typically handled by the context provider, which often uses useState
or useReducer
internally.
Example
import { createContext, useContext } from "react";const Context = createContext();const Child = () => {const context = useContext(Context);return <div>{context.data}</div>;};//Top most componentconst App = () => {return (<Context.Provider value={{ data: "Data from context!" }}><Child /></Context.Provider>);};
In the snippets above, we created a Context
object (i.e., the central data store) using the createContext()
function. This object can be accessed from anywhere in your application and provides two key components: Provider
and Consumer
(or the useContext
HHook).
The Provider component, positioned higher up in your component tree, acts as a higher-order component, passing down its value
prop to any nested components that need access. On the other hand, the Consumer component or the useContext
Hook allows child components to tap into this shared data.
When the Provider's value changes, any component using useContext
automatically re-renders to reflect the updated information, ensuring a consistent and synchronized user interface.
A significant advantage of useContext
is that it eliminates the need for prop drilling, leading to cleaner, more maintainable code. But be careful not to overuse it, as frequent updates with a lot of data can slow down your app.
If your context value is complex or changes frequently, consider using memoization techniques like useMemo
to avoid unnecessary re-rendering of consumer components. Let's now explore the useRef
Hook. Even though it's not directly tied to context, it often partners with useContext
.
#The useRef Hook
The useRef
Hook provides a way to access and interact with DOM nodes or store a mutable value that does not cause a re-render when updated. It creates a "box" that can hold a value, which remains unchanged even if the component re-renders multiple times.
Example
import { useRef } from 'react';function TextInput() {const inputRef = useRef(null);const focusInput = () => {inputRef.current.focus();};return (<div><input ref={inputRef} type="text" /><button onClick={focusInput}>Focus Input</button></div>);}
The inputRef
object is initialized by calling useRef(initialValue)
. The initialValue
can be any JavaScript value, including null, objects, arrays, or functions. The ref
object has a single property called .current
, which holds the stored value. You can access and modify this value directly without triggering a re-render of your component.
The useRef
Hook provides a way to directly access and manipulate DOM elements within functional components. It also stores values that persist across renders, which helps track input focus, timers, counters, or previous prop values.
When you want changes to be reflected in the UI, you need to update the ref's .current
value manually. Changes to this value won't automatically trigger re-renders like changes to any state managed with useState
.
The useRef
Hook isn't meant for managing UI state that triggers re-renders. Still, it's a valuable tool for storing persistent values or interacting directly with elements in your webpage.
Now, let's discuss the useId
Hook, a simple yet powerful tool for enhancing accessibility in React applications.
#The useId Hook
The Hook used is a utility for generating unique IDs that are stable across the server and the client renders. This is particularly important for building accessible web applications that rely on labels and relationships between elements, as it ensures consistent IDs for associating elements like labels and form inputs.
How it works:
- The Hook generates a unique ID string when a
useId
component is first rendered on the server. - The ID is then sent to the client as part of the HTML, and React reuses the same ID during hydration, ensuring consistency between server and client renders.
- Each
useId
call within a component generates a unique ID. These IDs remain stable across re-renders and updates unless the component is unmounted and remounted.
Example
import { useId } from 'react';function MyComponent() {const inputId = useId(); // Generate a unique IDconst labelId = useId();return (<div><label htmlFor={inputId}>My Input:</label><input id={inputId} type="text" /><p aria-describedby={labelId}>This is a description for the input field.</p></div>);}
In this example, the useId
Hook generates unique IDs for the input
and label
elements, ensuring a correct association between them for accessibility purposes.
The useId
simplifies creating components by generating unique IDs, essential for associating labels with React form inputs and other interactive elements. However, it's limited to React 18 and later versions and should not be used to generate keys for dynamic lists. Let’s discuss the useDeferredValue Hook next.
#The useDeferredValue Hook
The useDeferredValue
Hook is a performance optimization tool for deferring updating a value or re-rendering certain parts of your UI until the browser has time to render the updates.
How it works:
It takes two arguments: the value you want to defer (it can be of any type) and an optional timeout in milliseconds. React prioritizes rendering other parts of the UI that depend on the non-deferred value. This means that if the non-deferred value changes, React will immediately update those parts, while the parts that rely on the deferred value will update later in a less urgent manner.
Example
import { useState, useDeferredValue } from 'react';function MyComponent() {const [text, setText] = useState('');const deferredText = useDeferredValue(text);return (<div><input value={text} onChange={(e) => setText(e.target.value)} /><ExpensiveList items={deferredText} /></div>);}
In this example, ExpensiveList
depends on deferredText
. When you type something, the input box immediately shows what you typed. But, the ExpensiveList
, which might take longer to update, waits a moment before it shows the new text. This keeps the input box snappy and responsive while the slower part catches up in the background.
By deferring the re-rendering of expensive components, you can make your UI feel more responsive and avoid UI blocking. A user can interact with the UI elements (like typing in an input field) without experiencing lag, even while heavy computations or rendering occurs in the background.
The useDeferredValue
is particularly effective for optimizing common UI patterns like:
- Search Input with Results List.
- Large Lists or Tables.
- Complex Calculations.
The useDeferredValue
might not be very helpful if the problem you are trying to solve is not related to slow rendering. Overusing it can lead to users being served stale data.
#The useDebugValue Hook
The useDebugValue
Hook is primarily a developer tool in React. It's designed to enhance the debugging experience when working with custom Hooks. It allows you to display a label for your custom Hook in React DevTools, providing additional information about its state or value.
Here is how it works:
The useDebugValue
is usually called inside your custom Hook, usually at the top level. It takes two arguments: value
and formatter
(optional).
value
: The value you want to display in React DevTools. This can be the current state of your Hook, a calculated value, or any relevant information for debugging.formatter
: A function that takes the value and returns a formatted string. This allows you to customize how the value is displayed in DevTools.Example
import { useState, useDebugValue } from 'react';function useCounter(initialValue = 0) {const [count, setCount] = useState(initialValue);useDebugValue(count); // Display 'count' in DevTools// ... (rest of your custom Hook logic)}
The useDebugValue
helps developers gain more insight into the inner workings of their custom Hooks during development. It enhances the debugging experience in React DevTools, making it easier to identify and resolve state and data flow issues within custom Hooks.
The useDebugValue
shines when you use multiple custom Hooks within a component; it helps you distinguish between them in DevTools.
However, it doesn't change how your Hooks work, as it's purely a debugging tool. It only applies during development and doesn't affect the production build of your application. The following Hook we'll discuss is the useImperativeHandle Hook.
#The useImperativeHandle Hook
The useImperativeHandle
is a React Hook that allows you to customize the instance value exposed to parent components when using ref
.
To use useImperativeHandle
, first, your child component must be wrapped in the forwardRef
function. This allows the child component to receive a ref
object from its parent. In the child component, call the useImperativeHandle
with the following arguments:
ref
: The ref object passed down from the parent.createHandle
: A function that returns an object containing the methods or values you want to expose to the parent.dependencies (optional)
: An array of dependencies that trigger thecreateHandle
function to re-execute if they change.
Example
import { forwardRef, useRef, useImperativeHandle } from 'react';const ChildComponent = forwardRef((props, ref) => {const inputRef = useRef(null);useImperativeHandle(ref, () => ({focusInput: () => {inputRef.current.focus();},}));return <input ref={inputRef} />;});function ParentComponent() {const childRef = useRef(null);const handleClick = () => {childRef.current.focusInput();};return (<div><ChildComponent ref={childRef} /><button onClick={handleClick}>Focus Child Input</button></div>);}
In this example, the ParentComponent
can call focusInput
on the ChildComponent
instance through the ref
.
Using the useImperativeHandle
Hook, you can create controlled components where the parent can manage the child's state or behaviour. Instead of exposing the entire child component instance to the parent, you can expose only the necessary methods or properties.
The useImperativeHandle
Hook shines in creating custom form inputs with validation logic exposed to the parent component or controlling video or audio playback from a parent component.
While useImperativeHandle
offers robust control over child component behavior, it adds complexity that may be unnecessary for simple interactions, as props and callbacks typically suffice for most parent-child communication.
#The useLayoutEffect Hook
The useLayoutEffect
is a React Hook that is very similar to useEffect
but with a crucial difference in timing. While useEffect
runs after the browser has painted the screen (i.e., after your component's changes are visually reflected), useLayoutEffect
runs synchronously right before the browser paints.
How it works:
- First, the component renders with the initial state and props.
- React then updates the actual DOM based on the render.
- The useLayoutEffect Hook runs immediately after DOM changes but before the browser displays them.
- The Hook's code executes synchronously, allowing DOM measurements and manipulations.
- The browser then paints the updated UI, reflecting the changes made by
useLayoutEffect
.
Example
import { useState, useLayoutEffect } from 'react';function MyComponent() {const [dimensions, setDimensions] = useState({ width: 0, height: 0 });const myRef = useRef(null);useLayoutEffect(() => {const element = myRef.current;if (element) {setDimensions({width: element.offsetWidth,height: element.offsetHeight});}}, []); // Empty dependency array ensures it runs only once after initial renderreturn (<div ref={myRef}>This div's dimensions are: {dimensions.width} x {dimensions.height}</div>);}
The useLayoutEffect
allows you to read the layout from the DOM and make immediate changes in the next paint. This is crucial for scenarios like:
- Measuring the dimensions of a component after it's rendered.
- Adjusting the position or styling of an element based on its size or location.
- Building canvases and UIs that require complete control of the layout.
It is best used to calculate dimensions and positions, perform other layout-related operations, and synchronize animations with the DOM layout to avoid visual inconsistencies.
Because the useLayoutEffect
Hook runs synchronously, it can block the browser's rendering pipeline if the effect is computationally expensive. This can lead to a less responsive user experience. Let's discuss the useInsertionEffect
Hook next.
#The useInsertionEffect Hook
The useInsertionEffect
is a React Hook also introduced in React 18. It's primarily designed for library authors working with CSS-in-JS libraries. It allows you to inject styles or other DOM nodes into the document before layout calculations occur.
Here's how it works:
The Hook runs immediately after a component is mounted into the DOM before the browser performs layout calculations. This ensures that styles are applied before other effects (like useLayoutEffect
or useEffect
that might depend on those styles).
Example
import { useInsertionEffect } from 'react';useInsertionEffect(() => {// Code to inject styles or DOM nodesconst styleElement = document.createElement('style');document.head.appendChild(styleElement);return () => {// Cleanup function (optional) to remove the injected nodesdocument.head.removeChild(styleElement);};}, [dependencies]); // Optional dependency array
Similar to useLayoutEffect
, useInsertionEffect
executes synchronously. As a result, it shouldn't be used for asynchronous operations. The useInsertionEffect
Hook is designed to optimize the performance of CSS-in-JS libraries. Injecting styles before the browser calculates layouts prevents unnecessary and costly recalculations to ensure a smooth user experience.
While not typically required for everyday app development, it's a valuable tool for library authors seeking to enhance their CSS-in-JS solutions.
#The useSyncExternalStore
The useSyncExternalStore
Hook is designed for a specific purpose: subscribing to external data sources and efficiently synchronizing their updates with your React components. These external sources can be anything from browser local storage to a Redux store or even a custom state management solution.
The useSyncExternalStore
subscribes to an external store and retrieves its current state. It then re-renders your component whenever the store's state changes, ensuring your component displays the latest data. The subscription is cleaned up when the component is removed to prevent memory leaks.
Here's an example implementation:
import { useSyncExternalStore } from 'react';function MyComponent() {const storeState = useSyncExternalStore(store.subscribe, // Subscribe to the storestore.getSnapshot, // Get the current snapshot);return <div>{storeState}</div>;}
The useSyncExternalStore
provides a way to integrate with various external stores, abstracting away the complexities of subscription management. It also works with React's concurrent features, ensuring smooth rendering and responsiveness even with complex updates from the external store.
The useSyncExternalStore
shines in synchronizing browser storage (i.e., keeping your component's state in sync with localStorage or sessionStorage) and subscribing to external data streams like WebSockets or server-sent events. Let's discuss the useTransition Hook next.
#The useTransition Hook
The useTransition
Hook improves the user experience when dealing with state updates that could cause your UI to become unresponsive or slow. It allows you to mark specific state updates as "transitions," which signals to React that these updates are less urgent and can be interrupted if necessary.
How it works
- The
useTransition
returnsisPending
(boolean) to show if a transition is happening andstartTransition
(function) to mark updates as transitions. - React prioritizes urgent updates over transitions, ensuring a smooth user experience.
Example
import { useState, useTransition } from 'react';function MyComponent() {const [isPending, startTransition] = useTransition();const [value, setValue] = useState('');const handleChange = (event) => {startTransition(() => {setValue(event.target.value);});};return (<div><input type="text" value={value} onChange={handleChange} />{isPending ? <div>Loading...</div> : <HeavyComponent value={value} />}</div>);}
In this example, updating the value
state is marked as a transition. If the user types quickly into the input
, the input will remain responsive, and the HeavyComponent
will only update after a slight delay, this avoids rendering substandard UI.
The useTransition
makes your app feel smoother and more responsive, especially during complex updates. It prioritizes essential changes, like user input, and handles less urgent updates in the background without you needing to write extra code.
The useTransition
Hook optimizes specific scenarios within React 18+ applications, such as search inputs, extensive lists, and complex calculations where immediate UI updates aren't essential. However, it's important to use it judiciously, as it's not suitable for urgent updates and can introduce unnecessary complexity if overused.
Beyond the established Hooks, React boasts a set of experimental Hooks, offering a glimpse into the framework's future. These Hooks are still in development and might change, so they're not recommended for production use yet. However, they provide key features, like optimistic UI updates (showing changes before they're confirmed) and handling actions with built-in state management (useOptimistic
and useActionState
, respectively).
#The useOptimistic Hook
The useOptimistic
is designed to enhance the perceived responsiveness of your UI by allowing you to optimistically update the UI before an asynchronous action (like a network request) is complete. This means the user sees the expected outcome immediately, even though the action is still in progress. React will revert the UI to its previous state if the action fails.
How it works:
- The
useOptimistic
takes an action function and an initial optimistic value. - It returns the current value, a pending status indicator, and a reset function.
- The current value starts as optimistic, changes during the action, and updates with the actual result.
Example
import { useState, useOptimistic } from 'react';function MyComponent() {const [comments, setComments] = useState([]);const [newComment, addNewComment] = useOptimistic((text) => fetch('/api/comments', { method: 'POST', body: text }),'' // Initial optimistic value is an empty string);const handleSubmit = async (event) => {event.preventDefault();await addNewComment(newComment); // Trigger the actionsetComments([...comments, newComment]); // Update comments listsetNewComment(''); // Clear the input field};// ...}
#The useActionState Hook
The useActionState
is often used in conjunction with useOptimistic
. It simplifies the management of the state associated with an asynchronous action. It automatically tracks the action's status (pending, success, error) and provides convenient methods for handling its result.
How it works:
- First, you must provide an action function to
useActionState
to handle the asynchronous operation. - The
useActionState
returns the action's current state (idle
,pending
,success
, orerror
), a function to trigger the action (dispatch
), and a function to reset the state (reset
). - The returned
state
object contains the action's status, returned value, and potential errors.
Example
import { useActionState } from 'react';function MyComponent() {const [commentState, addComment] = useActionState((text) => fetch('/api/comments', { method: 'POST', body: text }));// ...}
The useOptimistic
and useActionState
Hooks work together to enhance user interactions. Optimistically, updating the UI before an action is complete gives the user a more responsive experience.
Additionally, useActionState
simplifies the handling of state associated with asynchronous actions, such as network requests. It automatically tracks the action's status (pending, success, error), reducing the need for manual state updates and providing built-in error management.
It's important to know that these experimental Hooks are still under development, may introduce complexity, and their behavior could change in future React versions, so use them with caution.
#Conclusion
React Hooks have revolutionized functional component development, offering a straightforward way to manage state, side effects, and other crucial aspects. From basic state updates with useState
to optimizing performance with useMemo
and useCallback
, these Hooks empower developers to build more efficient, maintainable, and interactive applications. While experimental Hooks like useOptimistic
and useActionState
are still evolving, they hint at an even brighter future for React development, promising enhanced user experiences and streamlined workflows.
Ready to explore React Hooks' full potential? Sign up for your free developer account and join the Hygraph developer community, where you'll find extensive resources, tutorials, and discussions to help you master these powerful tools. Stay curious, experiment with new Hooks, and unlock the full potential of React in your projects.
Blog Author
Chidi Eze
Technical writer
Chidi is a software engineer and technical writer with experience in building user-friendly applications and creating content around composable architectures.