React Hooks
Powerful features for functional components
What are Hooks?
Hooks are functions that let you use state and other React features in functional components.
Introduced in React 16.8, Hooks revolutionized React development by allowing you to use state and lifecycle features without writing class components.
Rules of Hooks
Important Rules
- Only call Hooks at the top level - Don't call Hooks inside loops, conditions, or nested functions
- Only call Hooks from React functions - Call Hooks from React functional components or custom Hooks
Built-in Hooks Overview
| Hook | Purpose |
|---|---|
useState |
Add state to functional components |
useEffect |
Perform side effects (data fetching, subscriptions) |
useContext |
Access React Context without nesting |
useReducer |
Manage complex state logic |
useRef |
Access DOM elements or persist values |
useMemo |
Memoize expensive calculations |
useCallback |
Memoize callback functions |
useState Hook
The most commonly used Hook for adding state to functional components.
Basic Usage
import { useState } from 'react';
function Counter() {
// Declare state variable
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Multiple State Variables
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [rememberMe, setRememberMe] = useState(false);
const handleSubmit = (e) => {
e.preventDefault();
console.log({ email, password, rememberMe });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<label>
<input
type="checkbox"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
/>
Remember me
</label>
<button type="submit">Login</button>
</form>
);
}
Functional Updates
function Counter() {
const [count, setCount] = useState(0);
// Good: Using functional update
const increment = () => {
setCount(prevCount => prevCount + 1);
};
// Multiple updates work correctly
const incrementThreeTimes = () => {
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={incrementThreeTimes}>+3</button>
</div>
);
}
Lazy Initial State
function ExpensiveComponent() {
// Function runs only once on mount
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation();
return initialState;
});
return <div>{state}</div>;
}
useEffect Hook
Perform side effects in functional components (data fetching, subscriptions, timers).
Basic useEffect
import { useState, useEffect } from 'react';
function DocumentTitle() {
const [count, setCount] = useState(0);
// Runs after every render
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<button onClick={() => setCount(count + 1)}>
Click {count} times
</button>
);
}
Effect with Dependencies
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// Runs only when userId changes
useEffect(() => {
async function fetchUser() {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
}
fetchUser();
}, [userId]); // Dependency array
return user ? <div>{user.name}</div> : <div>Loading...</div>;
}
Effect with Cleanup
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// Setup
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup function
return () => {
clearInterval(interval);
};
}, []); // Empty array = run once on mount
return <div>Seconds: {seconds}</div>;
}
Multiple Effects
function Component() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// Effect for document title
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
// Effect for localStorage
useEffect(() => {
localStorage.setItem('name', name);
}, [name]);
// Effect for API call
useEffect(() => {
fetchData();
}, []);
return <div>...</div>;
}
useContext Hook
Access React Context without nesting Consumer components.
Creating and Using Context
import { createContext, useContext, useState } from 'react';
// Create Context
const ThemeContext = createContext();
// Provider Component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Consumer Component
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
>
Toggle Theme
</button>
);
}
// App
function App() {
return (
<ThemeProvider>
<ThemedButton />
</ThemeProvider>
);
}
Multiple Contexts
const UserContext = createContext();
const ThemeContext = createContext();
function Component() {
const user = useContext(UserContext);
const theme = useContext(ThemeContext);
return (
<div style={{ color: theme.color }}>
Hello, {user.name}!
</div>
);
}
useReducer Hook
Alternative to useState for managing complex state logic.
Basic useReducer
import { useReducer } from 'react';
// Reducer function
function counterReducer(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:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
</div>
);
}
Complex State Management
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: Date.now(), text: action.text, done: false }];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
);
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.id);
default:
return state;
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, []);
const addTodo = (text) => {
dispatch({ type: 'ADD_TODO', text });
};
return <div>{/* Todo UI */}</div>;
}
useRef Hook
Access DOM elements directly or persist values across renders.
Accessing DOM Elements
import { useRef } from 'react';
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
</div>
);
}
Storing Mutable Values
function Timer() {
const intervalRef = useRef(null);
const [seconds, setSeconds] = useState(0);
const startTimer = () => {
intervalRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
};
const stopTimer = () => {
clearInterval(intervalRef.current);
};
return (
<div>
<p>Seconds: {seconds}</p>
<button onClick={startTimer}>Start</button>
<button onClick={stopTimer}>Stop</button>
</div>
);
}
Custom Hooks
Extract reusable logic into custom Hooks for better code organization.
useFetch Hook
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
const json = await response.json();
setData(json);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}
fetchData();
}, [url]);
return { data, loading, error };
}
// Usage
function UserList() {
const { data, loading, error } = useFetch('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
useLocalStorage Hook
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current theme: {theme}
</button>
);
}
useToggle Hook
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue(v => !v);
const setTrue = () => setValue(true);
const setFalse = () => setValue(false);
return [value, { toggle, setTrue, setFalse }];
}
// Usage
function Modal() {
const [isOpen, { toggle, setFalse }] = useToggle();
return (
<div>
<button onClick={toggle}>Open Modal</button>
{isOpen && (
<div className="modal">
<button onClick={setFalse}>Close</button>
</div>
)}
</div>
);
}