React Hooks, introduced in React 16.8, revolutionized how developers manage state and side effects in functional components. They allow you to "hook into" React features like state and lifecycle methods without writing class components. This blog post explores React Hooks in depth, covering all major hooks with practical, original examples to help you understand their usage.
What Are React Hooks?
Hooks are functions that let you use state and other React features in functional components. Before hooks, state and lifecycle methods were only available in class components. Hooks make code more reusable, readable, and easier to maintain by allowing logic to be organized into smaller, composable functions.
Why Use Hooks?
- Simpler Syntax: No need for class components or
thiskeyword. - Reusable Logic: Custom hooks let you share logic across components.
- Cleaner Code: Hooks reduce boilerplate and make components easier to test.
- Functional Paradigm: Aligns with modern JavaScript’s functional programming trends.
Core React Hooks
Let’s dive into the core hooks provided by React, with examples for each.
1. useState
The useState hook adds state to functional components. It returns an array with the current state and a function to update it.
Example: Counter App
A simple counter that increments or decrements a number.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h2>Counter: {count}</h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
Explanation:
useState(0)initializescountto 0.setCountupdates the state and triggers a re-render.- The component re-renders whenever
countchanges.
Advanced useState Example: Form Input
Managing form input state with an object.
import React, { useState } from 'react';
function UserForm() {
const [form, setForm] = useState({ name: '', email: '' });
const handleChange = (e) => {
setForm({ ...form, [e.target.name]: e.target.value });
};
return (
<div>
<input
type="text"
name="name"
value={form.name}
onChange={handleChange}
placeholder="Enter name"
/>
<input
type="email"
name="email"
value={form.email}
onChange={handleChange}
placeholder="Enter email"
/>
<p>Name: {form.name}</p>
<p>Email: {form.email}</p>
</div>
);
}
Explanation:
useStateholds an object withnameandemail.- The spread operator (
...form) preserves existing state while updating specific fields.
2. useEffect
The useEffect hook handles side effects, such as fetching data, subscriptions, or DOM manipulation. It runs after every render by default but can be controlled with a dependency array.
Example: Fetching Data
Fetch a list of users from a mock API.
import React, { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.json())
.then((data) => {
setUsers(data);
setLoading(false);
});
}, []); // Empty dependency array: runs once on mount
if (loading) return <p>Loading...</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Explanation:
useEffectruns the fetch call when the component mounts.- The empty dependency array (
[]) ensures it runs only once. setUsersupdates the state with fetched data, andsetLoadingtoggles the loading state.
Advanced useEffect Example: Window Resize Listener
Track the browser window’s width.
import React, { useState, useEffect } from 'react';
function WindowSize() {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize); // Cleanup
}, []); // Empty dependency array
return <p>Window Width: {windowWidth}px</p>;
}
Explanation:
useEffectadds an event listener for window resizing.- The cleanup function (returned by
useEffect) removes the listener to prevent memory leaks. - The dependency array ensures the effect runs only on mount and unmount.
3. useContext
The useContext hook accesses React’s context API, allowing you to share data without prop drilling.
Example: Theme Switcher
Toggle between light and dark themes using context.
import React, { useContext, useState } from 'react';
// Create Context
const ThemeContext = React.createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
function ThemeComponent() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<div style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
<p>Current Theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
}
function App() {
return (
<ThemeProvider>
<ThemeComponent />
</ThemeProvider>
);
}
Explanation:
ThemeContextholds the theme state andtoggleThemefunction.useContext(ThemeContext)provides access to the context values inThemeComponent.- The component dynamically updates styles based on the theme.
4. useReducer
The useReducer hook manages complex state logic, similar to Redux. It’s useful when state transitions involve multiple actions or conditions.
Example: Todo List
A simple todo list with add and remove functionality.
import React, { useReducer } from 'react';
const initialState = { todos: [] };
function reducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return { todos: [...state.todos, action.payload] };
case 'REMOVE_TODO':
return { todos: state.todos.filter((_, index) => index !== action.payload) };
default:
return state;
}
}
function TodoList() {
const [state, dispatch] = useReducer(reducer, initialState);
const [input, setInput] = useState('');
const addTodo = () => {
if (input.trim()) {
dispatch({ type: 'ADD_TODO', payload: input });
setInput('');
}
};
return (
<div>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add a todo"
/>
<button onClick={addTodo}>Add</button>
<ul>
{state.todos.map((todo, index) => (
<li key={index}>
{todo} <button onClick={() => dispatch({ type: 'REMOVE_TODO', payload: index })}>Remove</button>
</li>
))}
</ul>
</div>
);
}
Explanation:
useReducermanages thetodosarray with a reducer function.- Actions (
ADD_TODO,REMOVE_TODO) define state changes. dispatchtriggers state updates based on action types.
5. useCallback
The useCallback hook memoizes functions to prevent unnecessary re-creations, improving performance in scenarios involving child components.
Example: Memoized Callback
Pass a memoized function to a child component.
import React, { useState, useCallback } from 'react';
function Button({ onClick, label }) {
console.log(`${label} Button rendered`);
return <button onClick={onClick}>{label}</button>;
}
function CounterApp() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount((prev) => prev + 1);
}, []); // Empty dependency array: memoized forever
return (
<div>
<p>Count: {count}</p>
<Button onClick={increment} label="Increment" />
</div>
);
}
Explanation:
useCallbackmemoizes theincrementfunction.- Without
useCallback, theButtoncomponent would re-render unnecessarily on every state change. - The dependency array (
[]) ensures the function is only created once.
6. useMemo
The useMemo hook memoizes expensive computations, re-running them only when dependencies change.
Example: Expensive Calculation
Calculate the sum of numbers in a range.
import React, { useState, useMemo } from 'react';
function SumCalculator() {
const [range, setRange] = useState(1000);
const [otherState, setOtherState] = useState(0);
const sum = useMemo(() => {
console.log('Calculating sum...');
let total = 0;
for (let i = 1; i <= range; i++) {
total += i;
}
return total;
}, [range]); // Only recalculate if range changes
return (
<div>
<p>Sum of numbers up to {range}: {sum}</p>
<input
type="number"
value={range}
onChange={(e) => setRange(Number(e.target.value))}
/>
<button onClick={() => setOtherState(otherState + 1)}>
Other State: {otherState}
</button>
</div>
);
}
Explanation:
useMemocaches the sum calculation.- The calculation only runs when
rangechanges, not whenotherStateupdates. - This improves performance for computationally expensive operations.
7. useRef
The useRef hook creates a mutable reference that persists across renders. It’s commonly used for DOM access or storing values without triggering re-renders.
Example: Focus Input
Focus an input field on button click.
import React, { useRef } from 'react';
function FocusInput() {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus();
};
return (
<div>
<input type="text" ref={inputRef} placeholder="Type here" />
<button onClick={handleClick}>Focus Input</button>
</div>
);
}
Explanation:
useRefcreates a reference to the input element.inputRef.currentaccesses the DOM node to callfocus().
Advanced useRef Example: Tracking Previous State
Track the previous value of a state.
import React, { useState, useRef, useEffect } from 'react';
function PreviousValue() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count; // Update ref after each render
}, [count]);
return (
<div>
<p>Current Count: {count}</p>
<p>Previous Count: {prevCountRef.current}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Explanation:
useRefstores the previouscountvalue.useEffectupdates the ref after each render, preserving the previous state.
8. useLayoutEffect
The useLayoutEffect hook is similar to useEffect but runs synchronously after DOM updates, before the browser paints. It’s useful for DOM measurements.
Example: Measuring Element Size
Measure the dimensions of a div.
import React, { useState, useLayoutEffect, useRef } from 'react';
function ElementSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
const divRef = useRef(null);
useLayoutEffect(() => {
const rect = divRef.current.getBoundingClientRect();
setSize({ width: rect.width, height: rect.height });
}, []);
return (
<div>
<div ref={divRef} style={{ width: '200px', height: '100px', background: 'lightblue' }}>
Measure me!
</div>
<p>Width: {size.width}px, Height: {size.height}px</p>
</div>
);
}
Explanation:
useLayoutEffectmeasures the div’s size immediately after DOM updates.- It’s synchronous, ensuring accurate measurements before rendering.
9. useImperativeHandle
The useImperativeHandle hook customizes the instance value exposed to parent components when using ref. It’s used with forwardRef.
Example: Custom Input Focus
Expose a custom focus method to the parent.
import React, { useRef, useImperativeHandle, forwardRef } from 'react';
const CustomInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
customFocus: () => {
inputRef.current.focus();
inputRef.current.value = 'Custom Focus!';
},
}));
return <input ref={inputRef} placeholder="Custom input" />;
});
function App() {
const inputRef = useRef();
return (
<div>
<CustomInput ref={inputRef} />
<button onClick={() => inputRef.current.customFocus()}>
Trigger Custom Focus
</button>
</div>
);
}
Explanation:
useImperativeHandleexposes acustomFocusmethod to the parent.- The parent can call
customFocusvia the ref, which focuses the input and sets its value.
10. useDebugValue
The useDebugValue hook labels custom hooks in React DevTools, improving debugging.
Example: Custom Hook with Debug Value
A custom hook to track window size.
import React, { useState, useEffect, useDebugValue } from 'react';
function useWindowSize() {
const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });
useEffect(() => {
const handleResize = () => setSize({ width: window.innerWidth, height: window.innerHeight });
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
useDebugValue(`Width: ${size.width}, Height: ${size.height}`);
return size;
}
function WindowSizeDisplay() {
const { width, height } = useWindowSize();
return (
<p>
Window Size: {width}x{height}
</p>
);
}
Explanation:
useDebugValuedisplays the window size in React DevTools for theuseWindowSizehook.- This helps developers debug custom hooks.
Custom Hooks
Custom hooks are reusable functions that encapsulate logic using built-in hooks. They follow the naming convention useSomething.
Example: useToggle
A custom hook to toggle a boolean state.
import React, { useState } from 'react';
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue(!value);
return [value, toggle];
}
function ToggleComponent() {
const [isOn, toggle] = useToggle(false);
return (
<div>
<p>Toggle is {isOn ? 'ON' : 'OFF'}</p>
<button onClick={toggle}>Toggle</button>
</div>
);
}
Explanation:
useToggleencapsulates toggle logic usinguseState.- It returns the current state and a toggle function, reusable across components.
Rules of Hooks
To ensure hooks work correctly, follow these rules:
- Only Call Hooks at the Top Level: Don’t call hooks inside loops, conditions, or nested functions.
- Only Call Hooks from React Functions: Use hooks in functional components or custom hooks, not regular JavaScript functions.
- Use the ESLint plugin
eslint-plugin-react-hooksto enforce these rules.
Example of Incorrect Usage:
function BadComponent() {
if (true) {
const [state, setState] = useState(0); // Error: Hook in condition
}
}
Correct Usage:
function GoodComponent() {
const [state, setState] = useState(0); // Top-level hook
}
Best Practices
- Keep Hooks Simple: Break complex logic into custom hooks.
- Use Dependency Arrays Wisely: Specify all dependencies in
useEffectanduseMemoto avoid bugs. - Clean Up Effects: Return cleanup functions in
useEffectto prevent memory leaks. - Memoize When Necessary: Use
useCallbackanduseMemoonly when performance is an issue. - Debug with useDebugValue: Label custom hooks for better debugging.
Conclusion
React Hooks simplify state management and side effects in functional components, making React code more modular and maintainable. By mastering hooks like useState, useEffect, useContext, and others, you can build efficient, reusable components. Custom hooks take this further by encapsulating logic for reuse across your application.
Try experimenting with the examples provided to deepen your understanding. Happy coding!