Welcome back, future React pro! In our previous chapters, you mastered the foundational useState for managing simple component state and useEffect for handling side effects. You’ve built interactive components and started to see the power of React’s declarative approach.

But what happens when your state logic gets a bit more involved, or when you need to interact with the raw DOM, or even when you start noticing performance hiccups in larger applications? That’s where a deeper dive into React’s essential hooks comes in!

In this chapter, we’re going to unlock a new set of powerful tools: useRef for direct DOM interaction and mutable values, useReducer for managing complex state logic, and useCallback and useMemo for optimizing your component’s performance. Finally, we’ll learn the magic of creating your own custom hooks to reuse logic across your application. Get ready to level up your React skills and build even more robust and efficient applications!


1. useRef: Holding onto Mutable Values (and DOM Elements!)

Imagine you have a little “secret box” inside your component. You can put anything in it, change its contents, and retrieve them whenever you want. The cool part? Changing what’s inside this box doesn’t tell React to re-render your component. This secret box is essentially what useRef gives you!

What is useRef?

The useRef hook returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

Why is useRef Important?

useRef serves two primary purposes in modern React (as of React 18+, which is the current stable version as of 2026):

  1. Accessing the DOM directly: Sometimes, you need to interact with a browser’s DOM element directly – perhaps to focus an input, trigger an animation, or measure its size. React generally encourages a declarative approach, but for these specific “imperative” tasks, useRef is your friend.
  2. Storing mutable values that don’t trigger re-renders: If you have a value that needs to persist across renders but whose changes shouldn’t cause the component to update, useRef is perfect. Think of it as an instance variable in a class component, but for functional components.

How useRef Functions

When you call useRef(), React gives you an object that looks like this: { current: initialValue }. You can read and write to the .current property of this object.

Let’s see it in action!

Step-by-Step Implementation: Focusing an Input

We’ll create a simple input field and a button that, when clicked, automatically focuses the input.

  1. Start with a basic component:

    // src/components/FocusInput.jsx
    import React from 'react';
    
    function FocusInput() {
      return (
        <div>
          <h2>Focus Input Example</h2>
          <input type="text" />
          <button>Focus the Input</button>
        </div>
      );
    }
    
    export default FocusInput;
    

    And render it in your App.jsx (or wherever you’re building your practice components):

    // src/App.jsx
    import React from 'react';
    import FocusInput from './components/FocusInput';
    
    function App() {
      return (
        <div className="App">
          <h1>My React App</h1>
          <FocusInput />
        </div>
      );
    }
    
    export default App;
    
  2. Introduce useRef: Now, let’s bring in useRef to create a reference to our input element.

    // src/components/FocusInput.jsx
    import React, { useRef } from 'react'; // Don't forget to import useRef!
    
    function FocusInput() {
      const inputRef = useRef(null); // 1. Create a ref
    
      const handleFocusClick = () => {
        // 3. Access the DOM element through inputRef.current
        // and call its focus() method.
        inputRef.current.focus();
      };
    
      return (
        <div>
          <h2>Focus Input Example</h2>
          {/* 2. Attach the ref to the input element */}
          <input type="text" ref={inputRef} />
          <button onClick={handleFocusClick}>Focus the Input</button>
        </div>
      );
    }
    
    export default FocusInput;
    

    Explanation:

    • const inputRef = useRef(null);: We call useRef and initialize its .current property to null. React will automatically set inputRef.current to the actual DOM node once the component renders and the <input> element is available.
    • <input type="text" ref={inputRef} />: We pass the inputRef object to the ref prop of the <input> element. This is how React “connects” your ref object to the underlying DOM node.
    • inputRef.current.focus();: Inside our click handler, inputRef.current now holds a reference to the actual <input> DOM element. We can then call standard DOM methods like focus() on it.

    Try it out! Click the button, and you’ll see the cursor jump directly into the input field. Pretty neat, right?

Step-by-Step Implementation: Storing a Mutable Value

Let’s see how useRef can store a value that persists across renders without triggering them. This is useful for things like keeping track of previous state or a timer ID.

  1. Create a component to track render count:

    // src/components/RenderCounter.jsx
    import React, { useState, useEffect, useRef } from 'react';
    
    function RenderCounter() {
      const [count, setCount] = useState(0);
      const renderCount = useRef(0); // 1. Create a ref for our render count
    
      useEffect(() => {
        // 2. Increment the ref's current value on every render
        renderCount.current = renderCount.current + 1;
      }); // No dependency array, so it runs after every render
    
      return (
        <div>
          <h2>Render Counter</h2>
          <p>Current State Count: {count}</p>
          {/* 3. Display the ref's current value */}
          <p>Component Rendered (via useRef): {renderCount.current} times</p>
          <button onClick={() => setCount(count + 1)}>Increment State Count</button>
        </div>
      );
    }
    
    export default RenderCounter;
    

    Add RenderCounter to your App.jsx:

    // src/App.jsx
    // ... (imports)
    import RenderCounter from './components/RenderCounter';
    
    function App() {
      return (
        <div className="App">
          <h1>My React App</h1>
          <FocusInput />
          <hr />
          <RenderCounter />
        </div>
      );
    }
    
    export default App;
    

    Explanation:

    • renderCount.current = renderCount.current + 1;: Inside useEffect, we’re directly modifying the .current property of our renderCount ref.
    • Notice that renderCount.current updates in the console (if you were to console.log it) on every render, but the UI display of Component Rendered (via useRef): {renderCount.current} times only updates when setCount is called, which triggers a re-render. If we were to change renderCount.current without setCount being called, the UI wouldn’t reflect the change until something else caused a re-render. This demonstrates that changing a ref’s .current value does not trigger a re-render on its own.

Mini-Challenge: Previous Value Tracker

Create a component called PreviousValueTracker. It should:

  1. Have a state variable value initialized to 0.
  2. Display the current value.
  3. Display the previousValue (the value before the current one).
  4. Have a button to increment the value.
  5. Use useRef to store the previousValue.

Hint: You’ll need useEffect to update the previousValueRef.current after the component has rendered with the new value.


2. useReducer: A Powerful Alternative to useState for Complex State Logic

While useState is fantastic for simple state (like a number, string, or boolean), managing more complex state – especially when state updates depend on the previous state or involve multiple related sub-values – can sometimes lead to tangled useState calls and logic. Enter useReducer!

What is useReducer?

useReducer is a hook that provides an alternative to useState for managing state. It’s particularly useful when you have state logic that involves multiple sub-values or when the next state depends on the previous one. It’s conceptually similar to the pattern used in Redux, but for local component state.

Why is useReducer Important?

  1. Centralized State Logic: All state transition logic is contained within a single reducer function, making it easier to understand, test, and maintain.
  2. Predictable State Updates: Reducers are pure functions, meaning given the same input (current state and action), they will always produce the same output (new state). This predictability is great for debugging.
  3. Performance Optimization: When passing dispatch down to deeply nested children, dispatch itself is stable and won’t cause unnecessary re-renders, unlike passing a setState function created inline.
  4. Complex State Management: Ideal for scenarios like shopping carts, multi-step forms, or any state that changes in response to various “actions.”

How useReducer Functions

useReducer takes two (or three) arguments and returns an array with the current state and a dispatch function:

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

  • reducer function: This is a pure function that takes the currentState and an action object, and returns the newState. It describes how the state changes.
    function reducer(state, action) {
      switch (action.type) {
        case 'INCREMENT':
          return { count: state.count + 1 };
        case 'DECREMENT':
          return { count: state.count - 1 };
        case 'RESET':
          return { count: 0 };
        default:
          throw new Error();
      }
    }
    
  • initialState: The initial value of your state.
  • initFunction (optional): A function to lazily initialize the state. This can be useful for computing the initial state when it’s expensive.
  • state: The current state value, managed by the reducer.
  • dispatch function: A function you call with an action object to trigger a state update. The action object typically has a type property describing what happened.

Step-by-Step Implementation: A Complex Counter

Let’s build a counter that can increment, decrement, and reset, but also has a configurable step value. This is a great candidate for useReducer.

  1. Define the reducer and initialState:

    // src/components/ComplexCounter.jsx
    import React, { useReducer } from 'react';
    
    // 1. Define the initial state for our counter
    const initialState = {
      count: 0,
      step: 1
    };
    
    // 2. Define the reducer function
    // It takes the current state and an action, and returns the new state.
    function counterReducer(state, action) {
      switch (action.type) {
        case 'INCREMENT':
          return { ...state, count: state.count + state.step };
        case 'DECREMENT':
          return { ...state, count: state.count - state.step };
        case 'SET_STEP':
          // action.payload will contain the new step value
          return { ...state, step: action.payload };
        case 'RESET':
          return initialState; // Reset to the original initial state
        default:
          // It's good practice to throw an error for unknown actions
          throw new Error(`Unhandled action type: ${action.type}`);
      }
    }
    
    function ComplexCounter() {
      // We'll add the component logic here next
      return null; // Placeholder
    }
    
    export default ComplexCounter;
    

    Explanation of the reducer:

    • It’s a switch statement that checks action.type. This is a common pattern.
    • Each case returns a new state object. We use the spread operator (...state) to copy existing state properties and then override count or step. Crucially, reducers must be pure functions and should never mutate the original state object directly.
    • The SET_STEP action demonstrates how actions can carry additional data (often called payload) to update the state with specific values.
  2. Implement the component using useReducer:

    // src/components/ComplexCounter.jsx
    import React, { useReducer } from 'react';
    
    // ... (initialState and counterReducer from above)
    
    function ComplexCounter() {
      // 3. Use useReducer to get the current state and the dispatch function
      const [state, dispatch] = useReducer(counterReducer, initialState);
    
      return (
        <div>
          <h2>Complex Counter (useReducer)</h2>
          <p>Count: {state.count}</p>
          <p>Step: {state.step}</p>
    
          <button onClick={() => dispatch({ type: 'INCREMENT' })}>
            Increment by {state.step}
          </button>
          <button onClick={() => dispatch({ type: 'DECREMENT' })}>
            Decrement by {state.step}
          </button>
          <button onClick={() => dispatch({ type: 'RESET' })}>
            Reset
          </button>
    
          <div>
            <label htmlFor="step-input">Set Step: </label>
            <input
              id="step-input"
              type="number"
              value={state.step}
              onChange={(e) =>
                dispatch({ type: 'SET_STEP', payload: parseInt(e.target.value, 10) || 1 })
              }
              min="1"
            />
          </div>
        </div>
      );
    }
    
    export default ComplexCounter;
    

    Add ComplexCounter to your App.jsx:

    // src/App.jsx
    // ... (imports)
    import ComplexCounter from './components/ComplexCounter';
    
    function App() {
      return (
        <div className="App">
          <h1>My React App</h1>
          <FocusInput />
          <hr />
          <RenderCounter />
          <hr />
          <ComplexCounter />
        </div>
      );
    }
    
    export default App;
    

    Explanation:

    • const [state, dispatch] = useReducer(counterReducer, initialState);: We pass our counterReducer function and initialState to useReducer. It returns the current state (which will be { count: 0, step: 1 } initially) and a dispatch function.
    • onClick={() => dispatch({ type: 'INCREMENT' })}: Instead of setCount, we now call dispatch with an action object. The action object usually has a type property (a string) that the reducer uses to decide how to update the state.
    • dispatch({ type: 'SET_STEP', payload: parseInt(e.target.value, 10) || 1 }): For actions that need to carry data (like setting a new step value), we include a payload property.

    Play around with the counter. Change the step, then increment/decrement. Notice how all the state logic is neatly contained within the counterReducer function. This makes the ComplexCounter component itself much cleaner and focused on rendering.

Mini-Challenge: Simple Todo List with useReducer

Refactor a simple Todo List component to use useReducer instead of multiple useState calls. Your reducer should handle actions for:

  1. Adding a new todo (ADD_TODO, with a payload for the new todo text).
  2. Toggling a todo’s completion status (TOGGLE_TODO, with a payload for the todo’s ID).
  3. Deleting a todo (DELETE_TODO, with a payload for the todo’s ID).

Start with an initialState like this: [{ id: 1, text: 'Learn useReducer', completed: false }].

Hint: When adding a new todo, you’ll need to generate a unique ID. Using Date.now() is a quick way for simple examples.


3. useCallback and useMemo: Optimizing Performance

As your React applications grow, you might encounter scenarios where components re-render more often than necessary, leading to performance bottlenecks. useCallback and useMemo are React’s tools for memoization, a technique that helps optimize performance by “remembering” the result of expensive calculations or function definitions and reusing them if their dependencies haven’t changed.

Important Note: Don’t reach for these hooks for every component! They introduce their own overhead. Use them strategically when you’ve identified a performance issue, or when passing functions/objects to React.memoized child components.

What is Memoization?

Memoization is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.

Why are useCallback and useMemo Important?

  1. Preventing Unnecessary Re-renders: This is their primary use case. If a parent component re-renders, it might pass new references to functions or objects as props to its children. Even if the content of the function or object hasn’t changed, a new reference will cause a child component (especially one wrapped in React.memo) to re-render. useCallback and useMemo help maintain stable references.
  2. Avoiding Expensive Calculations: If you have a function that performs a complex, time-consuming calculation, useMemo can cache its result. If the inputs to that calculation haven’t changed, it will return the cached result instead of re-running the calculation.

How useCallback Functions

useCallback memoizes a function. It returns a memoized version of the callback function that only changes if one of the dependencies has changed.

const memoizedCallback = useCallback(callbackFunction, [dependencies]);

  • callbackFunction: The function you want to memoize.
  • dependencies: An array of values that the callbackFunction depends on. If any value in this array changes between renders, React will return a new version of the function. If the array is empty ([]), the function will only be created once.

How useMemo Functions

useMemo memoizes a value. It computes the value only when one of its dependencies changes.

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

  • computeExpensiveValue: A function that computes the value you want to memoize. useMemo will call this function and store its result.
  • dependencies: An array of values that the computation depends on. If any value in this array changes, useMemo will re-run the computeExpensiveValue function. If the array is empty ([]), the value will only be computed once.

Step-by-Step Implementation: useCallback with React.memo

Let’s illustrate how useCallback helps prevent unnecessary re-renders in child components that are optimized with React.memo.

  1. Create a memoized child component:

    // src/components/MemoizedButton.jsx
    import React from 'react';
    
    // React.memo is a Higher-Order Component that memoizes a component.
    // It will only re-render if its props have changed.
    const MemoizedButton = React.memo(({ onClick, children }) => {
      console.log(`Rendering MemoizedButton: ${children}`);
      return <button onClick={onClick}>{children}</button>;
    });
    
    export default MemoizedButton;
    

    Explanation: React.memo wraps our MemoizedButton. This means MemoizedButton will only re-render if its onClick prop (or children prop) changes.

  2. Create a parent component with and without useCallback:

    // src/components/CallbackExample.jsx
    import React, { useState, useCallback } from 'react';
    import MemoizedButton from './MemoizedButton';
    
    function CallbackExample() {
      const [count, setCount] = useState(0);
      const [message, setMessage] = useState('Hello');
    
      // 1. A regular function handler
      const handleIncrementRegular = () => {
        setCount(count + 1);
      };
    
      // 2. A memoized function handler using useCallback
      // This function will only be re-created if 'count' changes.
      const handleIncrementMemoized = useCallback(() => {
        setCount(count + 1);
      }, [count]); // Dependency array: depends on 'count'
    
      // A function that doesn't depend on any state, so it can be memoized once.
      const handleMessageChange = useCallback(() => {
        setMessage(prev => (prev === 'Hello' ? 'World' : 'Hello'));
      }, []); // Empty dependency array: never re-creates
    
      console.log('Rendering CallbackExample Parent');
    
      return (
        <div>
          <h2>useCallback Example</h2>
          <p>Count: {count}</p>
          <p>Message: {message}</p>
    
          {/* This button uses a regular function. */}
          {/* Even if MemoizedButton is wrapped in React.memo, this prop
              will cause it to re-render because handleIncrementRegular
              is a NEW function on every parent render. */}
          <MemoizedButton onClick={handleIncrementRegular}>
            Increment (Regular Fn)
          </MemoizedButton>
    
          {/* This button uses a useCallback memoized function. */}
          {/* MemoizedButton will ONLY re-render when 'count' changes for this prop. */}
          <MemoizedButton onClick={handleIncrementMemoized}>
            Increment (Memoized Fn)
          </MemoizedButton>
    
          {/* This button uses a useCallback memoized function with empty dependencies. */}
          {/* MemoizedButton will NEVER re-render due to this prop. */}
          <MemoizedButton onClick={handleMessageChange}>
            Toggle Message
          </MemoizedButton>
    
        </div>
      );
    }
    
    export default CallbackExample;
    

    Add CallbackExample to your App.jsx:

    // src/App.jsx
    // ... (imports)
    import CallbackExample from './components/CallbackExample';
    
    function App() {
      return (
        <div className="App">
          <h1>My React App</h1>
          <FocusInput />
          <hr />
          <RenderCounter />
          <hr />
          <ComplexCounter />
          <hr />
          <CallbackExample />
        </div>
      );
    }
    
    export default App;
    

    Observe: Open your browser’s developer console.

    • Initially, you’ll see “Rendering CallbackExample Parent” and all three “Rendering MemoizedButton” messages.
    • Click “Increment (Regular Fn)”. You’ll see “Rendering CallbackExample Parent” and all three “Rendering MemoizedButton” messages again, even though only one button was clicked and only count changed. This is because handleIncrementRegular is a brand new function reference on every render.
    • Click “Increment (Memoized Fn)”. You’ll see “Rendering CallbackExample Parent” and only the two “Rendering MemoizedButton” messages for the increment buttons. The “Toggle Message” button’s memoized function handleMessageChange has an empty dependency array, so its reference never changes, and its MemoizedButton child doesn’t re-render unnecessarily.
    • Click “Toggle Message”. You’ll see “Rendering CallbackExample Parent” and only the two “Rendering MemoizedButton” messages for the increment buttons. The message button re-renders because its children prop changed, but its onClick prop (from handleMessageChange) did not.

    This demonstrates how useCallback helps stabilize function references, which is crucial when working with React.memoized child components.

Step-by-Step Implementation: useMemo for Expensive Calculations

Now, let’s see useMemo in action to prevent re-running an expensive calculation.

  1. Create a component with an expensive calculation:

    // src/components/MemoExample.jsx
    import React, { useState, useMemo } from 'react';
    
    // This function simulates a very slow, CPU-intensive calculation.
    const findNthPrime = (num) => {
      console.log(`Calculating the ${num}th prime number...`);
      if (num < 1) return 0;
      let count = 0;
      let i = 1;
      while (count < num) {
        i++;
        let isPrime = true;
        for (let j = 2; j <= Math.sqrt(i); j++) {
          if (i % j === 0) {
            isPrime = false;
            break;
          }
        }
        if (isPrime) {
          count++;
        }
      }
      return i;
    };
    
    function MemoExample() {
      const [number, setNumber] = useState(1000);
      const [toggle, setToggle] = useState(false);
    
      // 1. WITHOUT useMemo: This calculation runs on every render.
      // const prime = findNthPrime(number);
    
      // 2. WITH useMemo: This calculation only runs when 'number' changes.
      const prime = useMemo(() => findNthPrime(number), [number]);
    
      console.log('Rendering MemoExample Parent');
    
      return (
        <div>
          <h2>useMemo Example: Expensive Calculation</h2>
          <input
            type="number"
            value={number}
            onChange={(e) => setNumber(parseInt(e.target.value, 10))}
          />
          <p>The {number}th prime number is: {prime}</p>
    
          <button onClick={() => setToggle(!toggle)}>
            Toggle State (Doesn't affect prime calculation)
          </button>
          <p>Toggle State: {toggle ? 'ON' : 'OFF'}</p>
        </div>
      );
    }
    
    export default MemoExample;
    

    Add MemoExample to your App.jsx:

    // src/App.jsx
    // ... (imports)
    import MemoExample from './components/MemoExample';
    
    function App() {
      return (
        <div className="App">
          <h1>My React App</h1>
          <FocusInput />
          <hr />
          <RenderCounter />
          <hr />
          <ComplexCounter />
          <hr />
          <CallbackExample />
          <hr />
          <MemoExample />
        </div>
      );
    }
    
    export default App;
    

    Observe:

    • Initially, you’ll see “Calculating the 1000th prime number…” and “Rendering MemoExample Parent”. The calculation might take a moment.
    • Change the number in the input field. The calculation will re-run, as number is in the dependency array for useMemo.
    • Click the “Toggle State” button. Notice that Rendering MemoExample Parent appears, but Calculating the Nth prime number... does not. This is because toggle is not in useMemo’s dependency array, so the memoized prime value is reused. If you comment out useMemo and uncomment the regular const prime = findNthPrime(number);, you’ll see the calculation run on every toggle!

    This clearly shows how useMemo saves us from re-doing expensive work when the relevant inputs haven’t changed.

Mini-Challenge: Filtered List with Memoization

Create a component that displays a list of objects (e.g., users with id, name, age).

  1. Have a state variable for the full list of users.
  2. Have a state variable for a filter term (e.g., a search string).
  3. Display a filtered list based on the filter term.
  4. Use useMemo to memoize the filtered list so it only re-calculates when the original list or the filter term changes.
  5. Include a button that adds a new user to the original list. Observe how this correctly triggers a re-calculation of the memoized filtered list.

4. Building Custom Hooks: Reusing Logic

You’ve learned about useState, useEffect, useContext, useRef, useReducer, useCallback, and useMemo. These are React’s built-in hooks. But what if you find yourself writing the same piece of stateful logic, or the same side effect pattern, in multiple components? That’s where custom hooks come in!

What are Custom Hooks?

A custom hook is a JavaScript function whose name starts with “use” and that can call other hooks. It allows you to extract component logic into reusable functions.

Why are Custom Hooks Important?

  1. Code Reusability: Share stateful logic between components without props, render props, or Higher-Order Components.
  2. Separation of Concerns: Keep your components focused purely on rendering UI, while the custom hooks handle the complex logic.
  3. Readability and Maintainability: Cleaner components and easier-to-understand logic, as related concerns are grouped.

How Custom Hooks Function

A custom hook is just a function that:

  • Starts with use (e.g., useToggle, useLocalStorage). This is a convention that allows React to know it follows the Rules of Hooks.
  • Calls one or more built-in (or other custom) hooks inside it.
  • Can take arguments and return values, just like any other JavaScript function.

Crucial Point: When you use a custom hook in multiple components, each component gets its own independent state. Custom hooks allow you to share logic, not share state.

Step-by-Step Implementation: useToggle Custom Hook

Let’s create a simple custom hook to manage a boolean state that can be toggled.

  1. Create the useToggle hook:

    // src/hooks/useToggle.js
    import { useState, useCallback } from 'react';
    
    function useToggle(initialState = false) {
      const [state, setState] = useState(initialState);
    
      // Memoize the toggle function to prevent unnecessary re-renders
      // if it's passed down to child components.
      const toggle = useCallback(() => setState(prev => !prev), []);
    
      return [state, toggle];
    }
    
    export default useToggle;
    

    Explanation:

    • It’s a regular function named useToggle.
    • It uses useState internally to manage the boolean state.
    • It returns an array [state, toggle], similar to useState.
    • We use useCallback for the toggle function. Since toggle doesn’t depend on any external variables (it uses prev => !prev), its dependency array is empty, meaning the toggle function reference will be stable across renders. This is a best practice for functions returned from hooks.
  2. Use the useToggle hook in a component:

    // src/components/ToggleComponent.jsx
    import React from 'react';
    import useToggle from '../hooks/useToggle'; // Import our new hook!
    
    function ToggleComponent() {
      // Use the custom hook just like a built-in hook
      const [isVisible, toggleVisibility] = useToggle(true);
      const [isDarkMode, toggleDarkMode] = useToggle(false);
    
      return (
        <div>
          <h2>Custom Hook Example: useToggle</h2>
          <p>Visibility: {isVisible ? 'Visible' : 'Hidden'}</p>
          <button onClick={toggleVisibility}>Toggle Visibility</button>
    
          <p>Dark Mode: {isDarkMode ? 'On' : 'Off'}</p>
          <button onClick={toggleDarkMode}>Toggle Dark Mode</button>
        </div>
      );
    }
    
    export default ToggleComponent;
    

    Add ToggleComponent to your App.jsx:

    // src/App.jsx
    // ... (imports)
    import ToggleComponent from './components/ToggleComponent';
    
    function App() {
      return (
        <div className="App">
          <h1>My React App</h1>
          <FocusInput />
          <hr />
          <RenderCounter />
          <hr />
          <ComplexCounter />
          <hr />
          <CallbackExample />
          <hr />
          <MemoExample />
          <hr />
          <ToggleComponent />
        </div>
      );
    }
    
    export default App;
    

    Explanation:

    • Notice how clean ToggleComponent is. It doesn’t need to know the implementation details of useState for toggling; it just uses useToggle.
    • Each call to useToggle in ToggleComponent creates an independent state for isVisible and isDarkMode. Toggling one doesn’t affect the other.

Mini-Challenge: useTimeout Custom Hook

Create a useTimeout custom hook that takes a callback function and a delay. It should:

  1. Set a timeout using setTimeout.
  2. Clear the timeout using clearTimeout when the component unmounts or when the delay/callback changes. (This is a job for useEffect!)
  3. The callback function should be memoized with useCallback if it’s passed as a dependency.

Hint:

// Inside your custom hook
useEffect(() => {
  const handler = setTimeout(callback, delay);
  return () => clearTimeout(handler);
}, [callback, delay]); // Dependencies

Common Pitfalls & Troubleshooting

  1. Forgetting Dependency Arrays: This is a classic! For useEffect, useCallback, and useMemo, an empty dependency array [] means “run once and never again” (or “memoize once”). Omitting the array means “run/recompute on every render.” Incorrect dependency arrays can lead to stale closures (functions that capture old values) or infinite loops.

    • Fix: Always be explicit. If a hook uses a value from its outer scope, include it in the dependency array. ESLint’s eslint-plugin-react-hooks (which comes with Create React App and Vite setups) will warn you about missing dependencies – pay attention to these warnings!
  2. Misusing useRef for UI Updates: Remember, changing ref.current does not trigger a re-render. If you need to update the UI based on a value, use useState or useReducer. useRef is for mutable values that don’t need to be reactive.

  3. Impure useReducer Functions: Your reducer function must be pure. It should:

    • Not perform any side effects (e.g., API calls, DOM manipulation).
    • Always return a new state object, never mutate the original state directly.
    • Given the same state and action, always return the same new state.
    • Fix: Use the spread operator (...state) to create new state objects.
  4. Over-optimizing with useCallback/useMemo: Don’t wrap every function or value in these hooks. They have their own overhead. Only use them when:

    • You are passing functions/objects to React.memoized child components to prevent unnecessary re-renders.
    • You have genuinely expensive computations that you want to avoid repeating.
    • Fix: Profile your application first (using React DevTools profiler) to identify actual performance bottlenecks before applying memoization.
  5. Not Following Rules of Hooks:

    • Only call Hooks at the top level of your function components or custom hooks.
    • Only call Hooks from React function components or custom hooks (not regular JavaScript functions).
    • Fix: Ensure your code adheres to these rules; ESLint helps here too.

Summary

Congratulations! You’ve just gained mastery over some of React’s most powerful and essential hooks.

Here’s a quick recap of what we covered:

  • useRef: Your go-to for directly interacting with DOM elements (like focusing an input) or storing mutable values that persist across renders without triggering re-renders.
  • useReducer: A robust alternative to useState for managing complex state logic, especially when state transitions depend on previous state or involve multiple related values. It centralizes state logic into a pure reducer function.
  • useCallback: Memoizes functions, ensuring that a function’s reference remains stable across renders unless its dependencies change. Crucial for optimizing child components wrapped in React.memo.
  • useMemo: Memoizes values, preventing expensive calculations from re-running on every render if their dependencies haven’t changed.
  • Custom Hooks: The ultimate tool for code reuse, allowing you to extract and share stateful logic across multiple components, keeping your UI components lean and focused.

Understanding these hooks will enable you to build more efficient, maintainable, and robust React applications. Remember, each hook is a specific tool for a specific job. Choose wisely!

What’s Next?

In the next chapter, we’ll shift our focus to rendering and reconciliation behavior in React. We’ll explore how React efficiently updates the DOM, what the Virtual DOM is, and how you can further leverage this knowledge to write even more performant applications. Get ready to peek under React’s hood!


References

  1. React Official Documentation - Hooks Overview: Learn about the motivation behind Hooks and their fundamental principles.
  2. React Official Documentation - useRef: Detailed reference for the useRef Hook.
  3. React Official Documentation - useReducer: Comprehensive guide on using useReducer for complex state.
  4. React Official Documentation - useCallback: Explanations and examples for memoizing functions.
  5. React Official Documentation - useMemo: How to memoize values and optimize expensive computations.
  6. React Official Documentation - Building Your Own Hooks: The definitive guide to creating custom hooks.

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.