React Hooks Complete Guide: useState to Custom Hooks
Master all React hooks from basics to advanced. Learn useState, useEffect, useContext, useReducer, useMemo, useCallback, and create custom hooks.
Moshiour Rahman
Advertisement
Introduction to React Hooks
Hooks let you use state and other React features without writing classes. They make components simpler and enable better code reuse.
Rules of Hooks
- Only call hooks at the top level (not inside loops, conditions, or nested functions)
- Only call hooks from React function components or custom hooks
useState
Basic Usage
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(prev => prev - 1)}>Decrement</button>
</div>
);
}
Object State
function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
const handleChange = (e) => {
const { name, value } = e.target;
setUser(prev => ({
...prev,
[name]: value
}));
};
return (
<form>
<input
name="name"
value={user.name}
onChange={handleChange}
/>
<input
name="email"
value={user.email}
onChange={handleChange}
/>
</form>
);
}
Array State
function TodoList() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const addTodo = () => {
setTodos(prev => [...prev, { id: Date.now(), text: input }]);
setInput('');
};
const removeTodo = (id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
};
const toggleTodo = (id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
};
return (
<div>
<input value={input} onChange={e => setInput(e.target.value)} />
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id} onClick={() => toggleTodo(todo.id)}>
{todo.text}
<button onClick={() => removeTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
useEffect
Side Effects
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Fetch data when userId changes
async function fetchUser() {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}
fetchUser();
}, [userId]); // Dependency array
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return <div>{user.name}</div>;
}
Cleanup Function
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// Cleanup function runs on unmount or before next effect
return () => clearInterval(interval);
}, []);
return <div>Seconds: {seconds}</div>;
}
function WindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <div>{size.width} x {size.height}</div>;
}
Effect Timing
// Runs after every render
useEffect(() => {
console.log('Every render');
});
// Runs once on mount
useEffect(() => {
console.log('Mount only');
}, []);
// Runs when dependencies change
useEffect(() => {
console.log('When value changes');
}, [value]);
useContext
Creating 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(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook for using context
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// Using the context
function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
>
Toggle Theme
</button>
);
}
// App wrapper
function App() {
return (
<ThemeProvider>
<ThemedButton />
</ThemeProvider>
);
}
useReducer
Complex State Logic
import { useReducer } from 'react';
const initialState = {
items: [],
loading: false,
error: null
};
function reducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, items: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] };
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload)
};
default:
return state;
}
}
function ItemList() {
const [state, dispatch] = useReducer(reducer, initialState);
const fetchItems = async () => {
dispatch({ type: 'FETCH_START' });
try {
const response = await fetch('/api/items');
const data = await response.json();
dispatch({ type: 'FETCH_SUCCESS', payload: data });
} catch (error) {
dispatch({ type: 'FETCH_ERROR', payload: error.message });
}
};
const addItem = (item) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
if (state.loading) return <div>Loading...</div>;
if (state.error) return <div>Error: {state.error}</div>;
return (
<ul>
{state.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
useMemo and useCallback
useMemo - Memoize Values
import { useMemo, useState } from 'react';
function ExpensiveComponent({ items, filter }) {
// Only recalculate when items or filter changes
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
const totalPrice = useMemo(() => {
return filteredItems.reduce((sum, item) => sum + item.price, 0);
}, [filteredItems]);
return (
<div>
<p>Total: ${totalPrice}</p>
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
useCallback - Memoize Functions
import { useCallback, useState, memo } from 'react';
// Memoized child component
const TodoItem = memo(({ todo, onToggle, onDelete }) => {
console.log('TodoItem render:', todo.id);
return (
<li>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
});
function TodoList() {
const [todos, setTodos] = useState([]);
// Memoize callbacks to prevent child re-renders
const handleToggle = useCallback((id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
}, []);
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</ul>
);
}
useRef
DOM References
import { useRef, useEffect } from 'react';
function TextInput() {
const inputRef = useRef(null);
useEffect(() => {
// Focus input on mount
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
function VideoPlayer() {
const videoRef = useRef(null);
const play = () => videoRef.current.play();
const pause = () => videoRef.current.pause();
return (
<div>
<video ref={videoRef} src="video.mp4" />
<button onClick={play}>Play</button>
<button onClick={pause}>Pause</button>
</div>
);
}
Mutable Values
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
const start = () => {
if (intervalRef.current) return;
intervalRef.current = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
intervalRef.current = null;
};
useEffect(() => {
return () => clearInterval(intervalRef.current);
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
Custom Hooks
useLocalStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return 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)} />;
}
useFetch
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url, { signal: controller.signal });
const json = await response.json();
setData(json);
setError(null);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
}
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// Usage
function Users() {
const { data, loading, error } = useFetch('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <ul>{data.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}
useDebounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Usage
function Search() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 500);
useEffect(() => {
if (debouncedQuery) {
// Fetch search results
}
}, [debouncedQuery]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
Summary
| Hook | Purpose |
|---|---|
| useState | Local state management |
| useEffect | Side effects and lifecycle |
| useContext | Access context values |
| useReducer | Complex state logic |
| useMemo | Memoize expensive calculations |
| useCallback | Memoize functions |
| useRef | DOM refs and mutable values |
React Hooks enable cleaner, more reusable code in functional components.
Advertisement
Moshiour Rahman
Software Architect & AI Engineer
Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.
Related Articles
Next.js 14 Tutorial: Complete Guide with App Router
Master Next.js 14 with App Router. Learn server components, data fetching, routing, server actions, and build full-stack React applications.
JavaScriptZustand: Lightweight React State Management Made Simple
Master Zustand for React state management. Learn store creation, async actions, middleware, persistence, and TypeScript integration patterns.
JavaScriptVite + React: Modern Frontend Development Setup
Master Vite for React development. Learn project setup, configuration, plugins, environment variables, and production optimization.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.