React Hooks revolutionized how we write React components by allowing us to use state and other React features in functional components. This guide covers all the essential hooks you need to know.
What Are React Hooks? #
Hooks are functions that let you “hook into” React state and lifecycle features from function components. They were introduced in React 16.8 and have become the standard way to write React components.
Why Use Hooks? #
Before hooks, you had to use class components to access state and lifecycle methods:
// Old way - Class Component
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Increment
</button>
</div>
);
}
}With hooks, the same component is much simpler:
// New way - Functional Component with Hooks
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}useState - Managing State #
useState is the most commonly used hook. It lets you add state to functional components.
Basic Usage #
import { useState } from 'react';
function Example() {
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 UserForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
return (
<form>
<input value={name} onChange={(e) => setName(e.target.value)} />
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<input value={age} onChange={(e) => setAge(Number(e.target.value))} />
</form>
);
}State with Objects #
function UserProfile() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
const updateName = (name) => {
setUser({ ...user, name }); // Spread to keep other properties
};
return (
<input
value={user.name}
onChange={(e) => updateName(e.target.value)}
/>
);
}State with Arrays #
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
setTodos([...todos, { id: Date.now(), text }]);
};
const removeTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
{todos.map(todo => (
<div key={todo.id}>
{todo.text}
<button onClick={() => removeTodo(todo.id)}>Delete</button>
</div>
))}
</div>
);
}useEffect - Side Effects #
useEffect lets you perform side effects in functional components. It replaces componentDidMount, componentDidUpdate, and componentWillUnmount.
Basic Usage #
import { useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<button onClick={() => setCount(count + 1)}>
Click me
</button>
);
}Running Once (Like componentDidMount) #
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(result => setData(result));
}, []); // Empty array means run once
return <div>{data}</div>;
}Cleanup (Like componentWillUnmount) #
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(interval); // Cleanup
}, []);
return <div>Timer: {count}</div>;
}Dependencies Array #
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
searchAPI(query).then(data => setResults(data));
}, [query]); // Re-run when query changes
return (
<ul>
{results.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}useContext - Consuming Context #
useContext lets you subscribe to React context without nesting.
Creating and Using Context #
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return <ThemedButton />;
}
function ThemedButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
style={{ background: theme === 'dark' ? '#333' : '#FFF' }}
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
Toggle Theme
</button>
);
}useRef - Accessing DOM Elements #
useRef creates a mutable reference that persists across renders.
Accessing DOM Elements #
import { useRef } from 'react';
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<>
<input ref={inputRef} />
<button onClick={focusInput}>Focus Input</button>
</>
);
}Storing Mutable Values #
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
const startTimer = () => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
};
const stopTimer = () => {
clearInterval(intervalRef.current);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={startTimer}>Start</button>
<button onClick={stopTimer}>Stop</button>
</div>
);
}useReducer - Complex State Logic #
useReducer is an alternative to useState for complex state logic.
import { useReducer } from 'react';
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();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { 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>
);
}useMemo - Performance Optimization #
useMemo memoizes expensive computations.
import { useMemo, useState } from 'react';
function ExpensiveComponent({ numbers }) {
const [multiplier, setMultiplier] = useState(1);
const expensiveCalculation = useMemo(() => {
console.log('Calculating...');
return numbers.reduce((sum, num) => sum + num, 0) * multiplier;
}, [numbers, multiplier]); // Only recalculate when these change
return (
<div>
<p>Result: {expensiveCalculation}</p>
<button onClick={() => setMultiplier(m => m + 1)}>
Increase Multiplier
</button>
</div>
);
}useCallback - Memoizing Functions #
useCallback memoizes functions to prevent unnecessary re-renders.
import { useCallback, useState } from 'react';
function Parent() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // Function doesn't change
return (
<div>
<Child onClick={handleClick} />
<button onClick={() => setOther(o => o + 1)}>Other: {other}</button>
</div>
);
}
const Child = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Increment</button>;
});Custom Hooks #
You can create your own hooks to reuse stateful logic.
Custom Hook Example #
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 App() {
const [name, setName] = useLocalStorage('name', '');
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
);
}Custom Fetch Hook #
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
// Usage
function Users() {
const { data, loading, error } = useFetch('https://api.example.com/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>
);
}Rules of Hooks #
- 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 functional components or custom hooks
// Wrong
function Example() {
if (condition) {
const [count, setCount] = useState(0); // Don't do this
}
}
// Correct
function Example() {
const [count, setCount] = useState(0);
if (condition) {
// Use the state here
}
}Common Patterns #
Fetching Data on Mount #
function DataComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchData() {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
setLoading(false);
}
fetchData();
}, []);
if (loading) return <div>Loading...</div>;
return <div>{data}</div>;
}Debouncing Input #
function SearchInput() {
const [searchTerm, setSearchTerm] = useState('');
const [debouncedTerm, setDebouncedTerm] = useState('');
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedTerm(searchTerm);
}, 500);
return () => clearTimeout(timer);
}, [searchTerm]);
useEffect(() => {
if (debouncedTerm) {
// Perform search
searchAPI(debouncedTerm);
}
}, [debouncedTerm]);
return (
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
);
}React Hooks simplify component logic and make code more reusable. Master these patterns to write better React applications.