Advanced Hooks and Custom Hooks in React

React’s hooks API introduced powerful ways to manage state and lifecycle methods in functional components. While basic hooks like useState and useEffect are essential, advanced hooks and custom hooks provide even greater control and reusability. In this section, we will explore advanced hooks like useReducer, useCallback, useMemo, and useRef, along with how to create custom hooks for reusable logic.


1. useReducer Hook

The useReducer hook is an alternative to useState for managing more complex state logic. It is especially useful when the state has multiple sub-values or when next state depends on the previous state.

Syntax:

				
					const [state, dispatch] = useReducer(reducer, initialState);

				
			
  • reducer: A function that determines how the state should change, given the current state and an action.
  • initialState: The starting state.
  • Example:
    				
    					import React, { useReducer } from 'react';
    
    function counterReducer(state, action) {
      switch (action.type) {
        case 'increment':
          return { count: state.count + 1 };
        case 'decrement':
          return { count: state.count - 1 };
        default:
          return state;
      }
    }
    
    function Counter() {
      const [state, dispatch] = useReducer(counterReducer, { count: 0 });
    
      return (
        <div>
          <p>Count: {state.count}</p>
          <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
          <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
        </div>
      );
    }
    
    				
    			

    In this example, useReducer manages the state of the counter. Instead of calling setState, we dispatch actions (increment or decrement), and the reducer determines how the state should change.


    2. useCallback Hook

    useCallback is used to memoize a function, ensuring it is not recreated on every render unless its dependencies change. This is useful when passing callbacks to child components, particularly to avoid unnecessary renders.

    Syntax:

    				
    					const memoizedCallback = useCallback(() => {
      // Function logic
    }, [dependencies]);
    
    				
    			

    Example:

    				
    					import React, { useState, useCallback } from 'react';
    
    function ParentComponent() {
      const [count, setCount] = useState(0);
    
      const handleClick = useCallback(() => {
        console.log('Button clicked');
      }, []);
    
      return (
        <div>
          <button onClick={handleClick}>Click Me</button>
          <button onClick={() => setCount(count + 1)}>Count: {count}</button>
        </div>
      );
    }
    
    				
    			
    In this example, the handleClick function is memoized and won’t be recreated on every render unless its dependencies (in this case, none) change. This can improve performance in scenarios where functions are passed down to child components.


    3. useMemo Hook

    useMemo is used to memoize a computed value, ensuring the value is recalculated only when its dependencies change. This is useful for performance optimization when expensive computations are involved.

    Syntax:

    				
    					const memoizedValue = useMemo(() => {
      return computeExpensiveValue(a, b);
    }, [a, b]);
    
    				
    			

    Example:

    				
    					import React, { useState, useMemo } from 'react';
    
    function ExpensiveCalculationComponent({ number }) {
      const computeFactorial = (n) => {
        console.log('Computing factorial');
        return n <= 0 ? 1 : n * computeFactorial(n - 1);
      };
    
      const factorial = useMemo(() => computeFactorial(number), [number]);
    
      return <div>Factorial of {number}: {factorial}</div>;
    }
    
    function App() {
      const [number, setNumber] = useState(5);
    
      return (
        <div>
          <ExpensiveCalculationComponent number={number} />
          <button onClick={() => setNumber(number + 1)}>Increment Number</button>
        </div>
      );
    }
    
    				
    			

    In this example, useMemo ensures that the factorial is recalculated only when the number changes, avoiding unnecessary recomputations.


    4. useRef Hook

    The useRef hook is primarily used to persist values across renders without causing re-renders. It can also be used to directly access and manipulate DOM elements.

    Syntax:

    				
    					const refContainer = useRef(initialValue);
    
    				
    			

    Example of Accessing DOM Elements:

    				
    					import React, { useRef } from 'react';
    
    function FocusInput() {
      const inputRef = useRef(null);
    
      const handleFocus = () => {
        inputRef.current.focus();
      };
    
      return (
        <div>
          <input ref={inputRef} type="text" />
          <button onClick={handleFocus}>Focus Input</button>
        </div>
      );
    }
    
    				
    			

    In this example, the useRef hook stores a reference to the input element, and the handleFocus function can programmatically focus the input when the button is clicked.


    5. Creating Custom Hooks

    Custom hooks allow you to extract and reuse logic across multiple components. This promotes cleaner code and reduces duplication. A custom hook is simply a function that uses one or more built-in hooks (like useState, useEffect, etc.) and returns values.

    Example of a Custom Hook:

    				
    					import { useState, useEffect } from 'react';
    
    function useWindowWidth() {
      const [width, setWidth] = useState(window.innerWidth);
    
      useEffect(() => {
        const handleResize = () => setWidth(window.innerWidth);
    
        window.addEventListener('resize', handleResize);
    
        return () => window.removeEventListener('resize', handleResize);
      }, []);
    
      return width;
    }
    
    function WindowWidthComponent() {
      const width = useWindowWidth();
    
      return <div>Window width: {width}px</div>;
    }
    
    				
    			

    Here’s how it works:

  • useWindowWidth is a custom hook that tracks the width of the browser window.
  • Inside the hook, useState is used to store the window width, and useEffect is used to set up and clean up the event listener for the window resize event.
  • The custom hook is reusable across components.

  • 6. Using Custom Hooks for Data Fetching

    A common use case for custom hooks is to manage data fetching logic, so it can be reused in multiple components.

    Example:

    				
    					import { useState, useEffect } from 'react';
    
    function useFetch(url) {
      const [data, setData] = useState(null);
      const [loading, setLoading] = useState(true);
      const [error, setError] = useState(null);
    
      useEffect(() => {
        fetch(url)
          .then(response => response.json())
          .then(data => {
            setData(data);
            setLoading(false);
          })
          .catch(err => {
            setError(err);
            setLoading(false);
          });
      }, [url]);
    
      return { data, loading, error };
    }
    
    function DataFetchingComponent() {
      const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts');
    
      if (loading) return <p>Loading...</p>;
      if (error) return <p>Error: {error.message}</p>;
    
      return (
        <ul>
          {data.map(post => (
            <li key={post.id}>{post.title}</li>
          ))}
        </ul>
      );
    }
    
    				
    			

    In this example:

  • The useFetch custom hook abstracts the data fetching logic, making it reusable for different URLs.
  • It manages the states for data, loading, and error, and can be used across components that need to fetch data.

  • Conclusion

    Advanced hooks like useReducer, useCallback, useMemo, and useRef offer more powerful state management, performance optimizations, and access to DOM elements in React components. Custom hooks allow for logic reusability, promoting cleaner, more maintainable code. By creating custom hooks, developers can extract logic such as data fetching, event handling, or any complex state management into reusable functions, improving code structure and maintainability.

    ×