Let's break down the most common React hooks, when they run, and their best practices.
Core React Hooks Overview
useState
: Manages local state in a functional component.
useEffect
: Handles side effects (like fetching data, subscriptions) and runs after render.
useContext
: Consumes a value from React's Context API.
useReducer
: An alternative touseState
for managing more complex state logic.
useCallback
: Memoizes functions to prevent unnecessary re-creations.
useMemo
: Memoizes computed values to prevent unnecessary recalculations.
useRef
: Accesses and persists a value across renders without causing re-renders.
useLayoutEffect
: LikeuseEffect
, but it fires synchronously after all DOM mutations, useful when measurements are needed before the browser paints.
useImperativeHandle
: Customizes the instance value of a ref passed to a parent component.
useDebugValue
: Can be used to display debug information in React DevTools.
React Hooks Lifecycle Diagram
Here's a detailed Mermaid diagram illustrating when each React hook runs during the lifecycle of a functional component:
graph TD;
A[Initial Render] --> B[Render Function Execution]
B --> C[useState Execution]
B --> D[useReducer Execution]
B --> E[useContext Execution]
B --> F[useRef Execution]
C --> G[Component DOM Rendered]
D --> G
E --> G
F --> G
G --> H[useEffect: Asynchronous, runs after DOM]
G --> I[useLayoutEffect: Synchronous, before painting]
H --> J[Component Re-render: State or Prop Change]
J --> B
J --> K[useMemo Execution: Optimizes expensive calculations]
J --> L[useCallback Execution: Memoizes functions]
K --> G
L --> G
Detailed Breakdown of Each Hook
1. useState
- What it does: Manages local component state.
- When it runs: During the initial render and re-renders if the state changes.
- Best Practices:
- Use for simple state management.
- Initialize with a value or a function (for expensive initialization).
- Avoid deeply nested state objects, which can lead to inefficient re-renders.
Example:
const [count, setCount] = useState(0);
2. useEffect
- What it does: Handles side effects like data fetching, subscriptions, and manual DOM manipulation.
- When it runs: After every render by default, but can be controlled with a dependency array.
- Best Practices:
- Always include a dependency array to control when the effect runs.
- Clean up side effects using the return function (e.g., removing event listeners, canceling network requests).
- Use it for async logic like fetching data or interacting with external systems.
Example:
useEffect(() => {
console.log("Effect runs after render");
return () => console.log("Cleanup before effect re-runs or unmount");
}, [count]); // Runs only when `count` changes
3. useContext
- What it does: Consumes a context value provided by a parent component.
- When it runs: Every time the parent context provider changes the value.
- Best Practices:
- Use with
React.createContext()
for global state management. - Avoid overusing context for low-level or frequent state updates, as it can trigger unnecessary re-renders.
Example:
const theme = useContext(ThemeContext);
4. useReducer
- What it does: Manages more complex state logic than
useState
by using a reducer function.
- When it runs: On initial render and re-renders when the
dispatch
function is called.
- Best Practices:
- Use when managing complex state transitions or when state depends on previous values.
- Keep reducer functions pure, without side effects.
Example:
const [state, dispatch] = useReducer(reducer, initialState);
5. useCallback
- What it does: Memoizes a function and only re-creates it when dependencies change.
- When it runs: Only when its dependencies change.
- Best Practices:
- Use when passing functions as props to avoid unnecessary re-renders.
- Use with care, as unnecessary memoization can hurt performance.
Example:
const memoizedCallback = useCallback(() => {
doSomething(count);
}, [count]);
6. useMemo
- What it does: Memoizes an expensive computation and only re-calculates when dependencies change.
- When it runs: Only when dependencies change.
- Best Practices:
- Use for expensive calculations or derived data to avoid recalculating on every render.
- Avoid using it for cheap calculations.
Example:
const expensiveCalculation = useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]);
7. useRef
- What it does: Provides a mutable object that persists across renders and does not cause re-renders when updated.
- When it runs: On every render, but it doesn't trigger re-renders.
- Best Practices:
- Use to store references to DOM elements or mutable values (like timers).
- Avoid using it for state that should trigger UI updates.
Example:
const inputRef = useRef();
8. useLayoutEffect
- What it does: Similar to
useEffect
, but fires synchronously after all DOM mutations and before the browser paints.
- When it runs: After the DOM is updated but before the browser repaints.
- Best Practices:
- Use for layout measurement (like element sizes, positions).
- Avoid blocking the user experience by doing long tasks synchronously.
Example:
useLayoutEffect(() => {
const height = elementRef.current.getBoundingClientRect().height;
console.log("Height:", height);
}, []);
9. useImperativeHandle
- What it does: Customizes the instance value that is exposed to parent components via
ref
.
- When it runs: When the component is mounted and when dependencies change.
- Best Practices:
- Use only when interacting with refs to control what parent components can access.
Example:
useImperativeHandle(ref, () => ({
scrollToTop() {
scrollRef.current.scrollTo(0, 0);
},
}));
10. useDebugValue
- What it does: Displays a label in React DevTools for custom hooks.
- When it runs: It runs when React DevTools is open.
- Best Practices:
- Use for debugging purposes in custom hooks.
Example:
useDebugValue(isOnline ? 'Online' : 'Offline');
Best Practices for Hooks Usage
- Keep hooks at the top level:
- Hooks should always be called at the top level of your component or custom hook to maintain the order of hook calls between renders. Don’t call them inside loops, conditions, or nested functions.
- Use hooks for isolated logic:
- If you notice a lot of repeated logic across multiple components, move it into a custom hook for reusability.
- Control dependencies in
useEffect
: - Always include the correct dependencies in the dependency array for
useEffect
. Missing a dependency could result in stale values, while adding unnecessary dependencies may cause excessive re-renders.
- Use
useCallback
anduseMemo
carefully: - Memoization can be useful, but overusing it without real performance concerns can add complexity and overhead.
- Avoid side effects in render:
- Don't put anything that causes side effects (e.g.,
setTimeout
,fetch
) directly inside the function body. Always useuseEffect
for side effects to ensure React handles it properly.
Detailed Flow of Hook Execution
To summarize the lifecycle and when each hook is executed, here's a final flow in natural language:
useState
,useReducer
,useRef
: Initialize values during the initial render.
- Render phase: The function body of the component runs. Any logs or values that don’t rely on effects or external systems will execute.
- DOM Update: The DOM gets updated.
- Post-render phase:
useEffect
anduseLayoutEffect
run after DOM updates.useEffect
runs asynchronously and is deferred to after the browser paints.useLayoutEffect
runs synchronously after the DOM has been updated but before the browser repaints.
- Subsequent renders: Re-execute hooks like
useCallback
,useMemo
, and
useEffect
based on their dependency arrays.
This setup will help you use React hooks optimally with a clear understanding of their roles in the component lifecycle.
TypeScript and React
As modern web applications grow in complexity, the need for a robust and scalable architecture becomes critical. React has long been the go-to framework for building dynamic user interfaces, but when combined with TypeScript, it can provide a much more scalable, maintainable, and type-safe development environment.
useEffect
React provides two primary hooks for side effects: useEffect and useLayoutEffect. While they may seem similar at first glance, they have distinct behaviors and different use cases. Understanding the differences will help you optimize performance and prevent unexpected bugs in your React applications.