DEV Community

Cover image for 15 Essential React 18 Hooks You Need to Know (With Examples)
Vivek Mengu
Vivek Mengu

Posted on

15 Essential React 18 Hooks You Need to Know (With Examples)

React 18 has supercharged the way we build functional components. Hooks are a game-changer, letting us write cleaner, more reusable, and easier-to-understand React components. In this blog, we'll dive into 15 essential hooks, from the basics to advanced techniques, and see how they can elevate your React projects.


1. useState

The useState hook allows you to add state variables to functional components. State represents dynamic data, which can change over time based on user interactions or other factors.

Use Case: Managing simple state like form inputs, counters, toggles, etc.

Example: Counter

import React, { useState } from 'react';

function Counter() {
    const [count, setCount] = useState(0); // Initial count is set to 0

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>Increment</button>
        </div>
    );
}

Enter fullscreen mode Exit fullscreen mode

Here, clicking the "Increment" button updates count, and useState re-renders the component whenever count changes.


2. useEffect

useEffect lets you perform side effects in functional components, like data fetching, setting up subscriptions, or manually modifying the DOM. By default, useEffect runs after every render, but it can be configured to run conditionally.

Use Case: Fetching data on component mount, setting up event listeners, or updating the document title.

Example: Fetching Data

import React, { useEffect, useState } from 'react';

function DataFetcher() {
    const [data, setData] = useState(null);

    useEffect(() => {
        fetch('https://api.example.com/data')
            .then(response => response.json())
            .then(setData);
    }, []); // Empty dependency array: runs once on mount

    return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}

Enter fullscreen mode Exit fullscreen mode

Here, the useEffect runs once after the component mounts (thanks to the empty dependency array), fetching data and storing it in data.


3. useContext

useContext lets you access global data (like themes or user authentication) that would otherwise have to be passed through many levels of components as props.

Use Case: Accessing global data, such as themes, languages, or authentication status, without prop drilling.

Example: Theme Context

import React, { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

function ThemedComponent() {
    const theme = useContext(ThemeContext);
    return <div>Current theme: {theme}</div>;
}

Enter fullscreen mode Exit fullscreen mode

By using useContext, you can easily access ThemeContext in any component without needing to pass theme as a prop through multiple layers.


4. useReducer

useReducer is an alternative to useState for managing more complex state logic. It works similarly to Redux and is especially helpful for managing states that rely on multiple transitions or involve complex data updates.

Use Case: Managing state with complex logic, like form handling or multi-step workflows.

Example: Counter with Reducer

import React, { useReducer } from 'react';

function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 };
        case 'decrement':
            return { count: state.count - 1 };
        default:
            throw new Error();
    }
}

function Counter() {
    const [state, dispatch] = useReducer(reducer, { count: 0 });

    return (
        <div>
            <p>Count: {state.count}</p>
            <button onClick={() => dispatch({ type: 'increment' })}>+</button>
            <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
        </div>
    );
}

Enter fullscreen mode Exit fullscreen mode

useReducer is ideal here because it separates action types and transitions, making it easy to extend with new actions without cluttering the code.


5. useRef

useRef is a versatile hook that provides a way to store mutable values without causing re-renders. It's commonly used to access and manipulate DOM elements directly.

Use Case: Storing references to DOM elements, managing timers, or holding previous state values.

Example: Focusing an Input Field

import React, { useRef } from 'react';

function FocusInput() {
    const inputRef = useRef();

    return (
        <div>
            <input ref={inputRef} />
            <button onClick={() => inputRef.current.focus()}>Focus</button>
        </div>
    );
}

Enter fullscreen mode Exit fullscreen mode

Here, inputRef.current.focus() directly manipulates the input element, focusing it without a re-render.


6. useMemo

useMemo is used to optimize performance by memoizing the result of expensive calculations between renders. It only recalculates when its dependencies change.

Use Case: Improving performance for costly calculations or derived values that don’t need recalculating each render.

Example: Expensive Calculation

import React, { useMemo, useState } from 'react';

function ExpensiveCalculation({ value }) {
    const result = useMemo(() => {
        return performExpensiveCalculation(value);
    }, [value]);

    return <div>Result: {result}</div>;
}

Enter fullscreen mode Exit fullscreen mode

By memoizing performExpensiveCalculation, React only recalculates when value changes, which can significantly improve performance.


7. useCallback

useCallback is similar to useMemo but memoizes functions. It's useful for passing stable references of callback functions, especially to child components that rely on shallow comparison.

Use Case: Preventing unnecessary re-renders when passing functions as props to child components.

Example: Passing Stable Callback

import React, { useCallback, useState } from 'react';

function Parent() {
    const [count, setCount] = useState(0);

    const increment = useCallback(() => {
        setCount(c => c + 1);
    }, []);

    return <Child increment={increment} />;
}

function Child({ increment }) {
    return <button onClick={increment}>Increment</button>;
}

Enter fullscreen mode Exit fullscreen mode

Using useCallback ensures that increment retains the same reference between renders, avoiding unnecessary re-renders of Child.


8. useLayoutEffect

useLayoutEffect is like useEffect but runs synchronously after all DOM mutations. It’s useful when you need to read layout information and make immediate updates to prevent visual flickers.

Use Case: Manipulating the DOM immediately after render to avoid flickering or incorrect initial layout.

Example: Adjusting Layout

import React, { useLayoutEffect, useRef } from 'react';

function ResizableBox() {
    const boxRef = useRef();

    useLayoutEffect(() => {
        const box = boxRef.current;
        box.style.width = '200px';
        box.style.height = '200px';
    }, []);

    return <div ref={boxRef} style={{ backgroundColor: 'lightblue' }} />;
}

Enter fullscreen mode Exit fullscreen mode

useLayoutEffect runs before the component is painted to the screen, ensuring layout adjustments happen immediately.


9. useImperativeHandle

useImperativeHandle customizes the ref value exposed to a parent component when using forwardRef. This allows you to limit what actions the parent can perform on a child component.

Use Case: Exposing specific methods or properties to parent components.

Example: Custom Input with Focus Control

import React, { useImperativeHandle, forwardRef, useRef } from 'react';

const CustomInput = forwardRef((props, ref) => {
    const inputRef = useRef();

    useImperativeHandle(ref, () => ({
        focus: () => {
            inputRef.current.focus();
        }
    }));

    return <input ref={inputRef} />;
});

function Parent() {
    const ref = useRef();

    return (
        <div>
            <CustomInput ref={ref} />
            <button onClick={() => ref.current.focus()}>Focus Input</button>
        </div>
    );
}

Enter fullscreen mode Exit fullscreen mode

With useImperativeHandle, you expose only specific methods like focus, providing better control to the parent.


10. useDebugValue

useDebugValue helps in adding custom labels for your custom hooks to make debugging easier in React DevTools.

Use Case: Debugging custom hooks more effectively.

Example: Debugging a Custom Hook

import React, { useDebugValue, useState } from 'react';

function useCustomHook(value) {
    useDebugValue(value > 10 ? 'High' : 'Low');
    return value;
}

function Component() {
    const value = useCustomHook(15);
    return <div>Value: {value}</div>;
}

Enter fullscreen mode Exit fullscreen mode

In React DevTools, you’ll see a label "High" or "Low" based on the hook’s state, making it clearer when debugging.


11. useId

useId generates unique IDs that are stable across the client and server. This is especially helpful when creating accessible components that require unique identifiers (e.g., for aria labels or htmlFor attributes), without worrying about ID collisions.

Use Case: Associating labels with form elements or ARIA attributes in a way that’s unique across all instances of a component.

Example: Accessible Form Fields

import React, { useId } from 'react';

function FormField() {
    const id = useId();  // Generates a unique ID for this component instance

    return (
        <div>
            <label htmlFor={id}>Username</label>
            <input id={id} type="text" />
        </div>
    );
}

Enter fullscreen mode Exit fullscreen mode

By using useId, every FormField instance will have a unique id, which is essential for accessibility and avoids conflicts when rendering multiple instances of the same component.


12. useTransition

useTransition provides a way to prioritize rendering so that the UI remains responsive during updates that may take longer. It allows you to mark state updates as "transitions" to avoid blocking the main thread and keep the UI interactive.

Use Case: When triggering an update that could delay or "freeze" the UI, such as filtering or sorting a large data set.

Example: Filtering a Large List

import React, { useState, useTransition } from 'react';

function SearchableList({ items }) {
    const [query, setQuery] = useState('');
    const [isPending, startTransition] = useTransition();

    const handleSearch = (e) => {
        const value = e.target.value;
        setQuery(value);

        startTransition(() => {
            // Expensive filtering operation marked as a transition
            const filteredItems = items.filter(item => item.includes(value));
            setFilteredItems(filteredItems);
        });
    };

    return (
        <div>
            <input type="text" value={query} onChange={handleSearch} placeholder="Search..." />
            {isPending ? <p>Loading...</p> : <List items={filteredItems} />}
        </div>
    );
}

Enter fullscreen mode Exit fullscreen mode

Here, useTransition ensures that the UI remains responsive by deferring the expensive filtering calculation until it has time to complete. While the filtering occurs, isPending will be true, showing a "Loading..." message.


13. useDeferredValue

useDeferredValue lets you delay updates to a value until the main work is done, helping to prevent UI flickering and maintain smooth rendering for low-priority updates.

Use Case: Displaying non-urgent changes, such as search suggestions, after critical UI tasks are completed.

Example: Deferred Search Suggestions

import React, { useState, useDeferredValue } from 'react';

function SearchInput() {
    const [input, setInput] = useState('');
    const deferredInput = useDeferredValue(input); // Defers this value update

    return (
        <div>
            <input 
                type="text" 
                value={input} 
                onChange={(e) => setInput(e.target.value)} 
                placeholder="Type to search..." 
            />
            <SearchSuggestions query={deferredInput} />
        </div>
    );
}

Enter fullscreen mode Exit fullscreen mode

In this example, SearchSuggestions only receives deferredInput, which will lag slightly behind input. This way, SearchSuggestions will update only after more immediate tasks are complete, reducing flickering as the user types quickly.


14. useSyncExternalStore

useSyncExternalStore ensures that React components stay synchronized with external sources of data (such as global state managers or APIs) and handle external updates correctly in concurrent rendering.

Use Case: Integrating with third-party data sources, libraries, or global stores, where changes need to reflect immediately in the component.

Example: Integrating with a Global Store

import React, { useSyncExternalStore } from 'react';
import { subscribe, getState } from './globalStore';

function StoreSubscriber() {
    // Uses the global store’s subscribe and getState functions
    const state = useSyncExternalStore(subscribe, getState);

    return <div>Current Store State: {JSON.stringify(state)}</div>;
}

Enter fullscreen mode Exit fullscreen mode

Here, useSyncExternalStore allows StoreSubscriber to stay in sync with the global store without additional state management code, even in React’s concurrent mode.


15. useInsertionEffect

useInsertionEffect is a low-level hook for injecting styles into the DOM. It runs before DOM mutations, making it ideal for dynamically injecting styles in libraries where critical CSS rendering order matters.

Use Case: Adding styles in libraries or handling third-party CSS where style injection order is critical.

Example: Injecting Styles Dynamically

import React, { useInsertionEffect } from 'react';

function DynamicStyleComponent() {
    useInsertionEffect(() => {
        const style = document.createElement('style');
        style.textContent = '.dynamic { color: blue; }';
        document.head.appendChild(style);

        return () => {
            document.head.removeChild(style);
        };
    }, []);

    return <div className="dynamic">Styled Text</div>;
}

Enter fullscreen mode Exit fullscreen mode

This hook ensures that the .dynamic style is applied immediately before the DOM is rendered, avoiding flickering or style conflicts. It’s particularly valuable in complex styling scenarios where style order matters.


Conclusion

React 18 has introduced a new era of functional component development. Hooks provide a flexible and efficient way to manage state, side effects, and other complex behaviour's within your components.

Each of these React hooks serves a unique purpose that helps you tackle specific challenges in building modern, efficient, and user-friendly applications. By understanding when and why to use them, you can unlock powerful functionality within your components, handle asynchronous rendering smoothly, and maintain a responsive and accessible UI.

If you know any such example to leverage the hooks effectively please share in the comments below. Your insights will be valuable to other developers.

Happy coding! 🚀

Top comments (4)

Collapse
 
okumarkets profile image
Oku Markets

Good post!

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

The fact alone that there are 15 hooks you need to learn is what makes React a terrible library when compared to more modern ones. The learning curve on React is too steep.

Collapse
 
vivekmengu016 profile image
Vivek Mengu

I know hooks can seem like a lot to learn, but they really add a ton of flexibility once you get used to them. I hope this post helps you find this examples easy to follow.

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

I'll stick to Svelte instead. One of the top performers nowadays, plus its learning curve is far easier.