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):
- 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,
useRefis your friend. - 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,
useRefis 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.
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;Introduce
useRef: Now, let’s bring inuseRefto 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 calluseRefand initialize its.currentproperty tonull. React will automatically setinputRef.currentto the actual DOM node once the component renders and the<input>element is available.<input type="text" ref={inputRef} />: We pass theinputRefobject to therefprop 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.currentnow holds a reference to the actual<input>DOM element. We can then call standard DOM methods likefocus()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.
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
RenderCounterto yourApp.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;: InsideuseEffect, we’re directly modifying the.currentproperty of ourrenderCountref.- Notice that
renderCount.currentupdates in the console (if you were toconsole.logit) on every render, but the UI display ofComponent Rendered (via useRef): {renderCount.current} timesonly updates whensetCountis called, which triggers a re-render. If we were to changerenderCount.currentwithoutsetCountbeing called, the UI wouldn’t reflect the change until something else caused a re-render. This demonstrates that changing a ref’s.currentvalue does not trigger a re-render on its own.
Mini-Challenge: Previous Value Tracker
Create a component called PreviousValueTracker. It should:
- Have a state variable
valueinitialized to0. - Display the current
value. - Display the
previousValue(the value before the current one). - Have a button to increment the
value. - Use
useRefto store thepreviousValue.
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?
- Centralized State Logic: All state transition logic is contained within a single
reducerfunction, making it easier to understand, test, and maintain. - 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.
- Performance Optimization: When passing
dispatchdown to deeply nested children,dispatchitself is stable and won’t cause unnecessary re-renders, unlike passing asetStatefunction created inline. - 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);
reducerfunction: This is a pure function that takes thecurrentStateand anactionobject, and returns thenewState. 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.dispatchfunction: A function you call with anactionobject to trigger a state update. Theactionobject typically has atypeproperty 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.
Define the
reducerandinitialState:// 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
switchstatement that checksaction.type. This is a common pattern. - Each
casereturns a new state object. We use the spread operator (...state) to copy existing state properties and then overridecountorstep. Crucially, reducers must be pure functions and should never mutate the originalstateobject directly. - The
SET_STEPaction demonstrates how actions can carry additional data (often calledpayload) to update the state with specific values.
- It’s a
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
ComplexCounterto yourApp.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 ourcounterReducerfunction andinitialStatetouseReducer. It returns the currentstate(which will be{ count: 0, step: 1 }initially) and adispatchfunction.onClick={() => dispatch({ type: 'INCREMENT' })}: Instead ofsetCount, we now calldispatchwith anactionobject. Theactionobject usually has atypeproperty (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 apayloadproperty.
Play around with the counter. Change the step, then increment/decrement. Notice how all the state logic is neatly contained within the
counterReducerfunction. This makes theComplexCountercomponent 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:
- Adding a new todo (
ADD_TODO, with apayloadfor the new todo text). - Toggling a todo’s completion status (
TOGGLE_TODO, with apayloadfor the todo’s ID). - Deleting a todo (
DELETE_TODO, with apayloadfor 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?
- 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.useCallbackanduseMemohelp maintain stable references. - Avoiding Expensive Calculations: If you have a function that performs a complex, time-consuming calculation,
useMemocan 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 thecallbackFunctiondepends 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.useMemowill call this function and store its result.dependencies: An array of values that the computation depends on. If any value in this array changes,useMemowill re-run thecomputeExpensiveValuefunction. 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.
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.memowraps ourMemoizedButton. This meansMemoizedButtonwill only re-render if itsonClickprop (orchildrenprop) changes.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
CallbackExampleto yourApp.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
countchanged. This is becausehandleIncrementRegularis 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
handleMessageChangehas an empty dependency array, so its reference never changes, and itsMemoizedButtonchild 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
childrenprop changed, but itsonClickprop (fromhandleMessageChange) did not.
This demonstrates how
useCallbackhelps stabilize function references, which is crucial when working withReact.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.
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
MemoExampleto yourApp.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
numberis in the dependency array foruseMemo. - Click the “Toggle State” button. Notice that
Rendering MemoExample Parentappears, butCalculating the Nth prime number...does not. This is becausetoggleis not inuseMemo’s dependency array, so the memoizedprimevalue is reused. If you comment outuseMemoand uncomment the regularconst prime = findNthPrime(number);, you’ll see the calculation run on every toggle!
This clearly shows how
useMemosaves 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).
- Have a state variable for the full list of users.
- Have a state variable for a filter term (e.g., a search string).
- Display a filtered list based on the filter term.
- Use
useMemoto memoize the filtered list so it only re-calculates when the original list or the filter term changes. - 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?
- Code Reusability: Share stateful logic between components without props, render props, or Higher-Order Components.
- Separation of Concerns: Keep your components focused purely on rendering UI, while the custom hooks handle the complex logic.
- 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.
Create the
useTogglehook:// 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
useStateinternally to manage the boolean state. - It returns an array
[state, toggle], similar touseState. - We use
useCallbackfor thetogglefunction. Sincetoggledoesn’t depend on any external variables (it usesprev => !prev), its dependency array is empty, meaning thetogglefunction reference will be stable across renders. This is a best practice for functions returned from hooks.
- It’s a regular function named
Use the
useTogglehook 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
ToggleComponentto yourApp.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
ToggleComponentis. It doesn’t need to know the implementation details ofuseStatefor toggling; it just usesuseToggle. - Each call to
useToggleinToggleComponentcreates an independent state forisVisibleandisDarkMode. Toggling one doesn’t affect the other.
- Notice how clean
Mini-Challenge: useTimeout Custom Hook
Create a useTimeout custom hook that takes a callback function and a delay. It should:
- Set a timeout using
setTimeout. - Clear the timeout using
clearTimeoutwhen the component unmounts or when the delay/callback changes. (This is a job foruseEffect!) - The
callbackfunction should be memoized withuseCallbackif 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
Forgetting Dependency Arrays: This is a classic! For
useEffect,useCallback, anduseMemo, 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!
- Fix: Always be explicit. If a hook uses a value from its outer scope, include it in the dependency array. ESLint’s
Misusing
useReffor UI Updates: Remember, changingref.currentdoes not trigger a re-render. If you need to update the UI based on a value, useuseStateoruseReducer.useRefis for mutable values that don’t need to be reactive.Impure
useReducerFunctions: 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
statedirectly. - Given the same
stateandaction, always return the same newstate. - Fix: Use the spread operator (
...state) to create new state objects.
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.
- You are passing functions/objects to
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 touseStatefor managing complex state logic, especially when state transitions depend on previous state or involve multiple related values. It centralizes state logic into a purereducerfunction.useCallback: Memoizes functions, ensuring that a function’s reference remains stable across renders unless its dependencies change. Crucial for optimizing child components wrapped inReact.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
- React Official Documentation - Hooks Overview: Learn about the motivation behind Hooks and their fundamental principles.
- React Official Documentation -
useRef: Detailed reference for theuseRefHook. - React Official Documentation -
useReducer: Comprehensive guide on usinguseReducerfor complex state. - React Official Documentation -
useCallback: Explanations and examples for memoizing functions. - React Official Documentation -
useMemo: How to memoize values and optimize expensive computations. - 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.