← Back to Home

React Hooks Complete Guide

React Hooks Complete Guide

Comprehensive guide to React Hooks with practical examples, best practices, and common patterns. Master state management and side effects in functional components.

Prerequisites

This guide assumes familiarity with React fundamentals including components, props, and JSX. All examples use modern JavaScript (ES6+) and functional components.

React Version: 16.8+ | Syntax: Functional Components with Hooks

Basic Hooks

The fundamental hooks that form the foundation of React's functional component state and lifecycle management.

useState

Basic State Management

// Simple state with primitive values
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [isVisible, setIsVisible] = useState(true);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(prev => prev + 1)}>
Increment (functional update)
</button>
</div>
);
}

Use functional updates when the new state depends on the previous state to avoid stale closures.

Complex State (Objects & Arrays)

// Object state
function UserProfile() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
const updateName = (newName) => {
setUser(prevUser => ({
...prevUser,
name: newName
}));
};
// Array state
const [items, setItems] = useState([]);
const addItem = (item) => {
setItems(prevItems => [...prevItems, item]);
};
const removeItem = (index) => {
setItems(prevItems => prevItems.filter((_, i) => i !== index));
};
return /* JSX */;
}

💡 State Update Rules:

  • • Always create new objects/arrays instead of mutating existing ones
  • • Use spread operator (...) for shallow copying
  • • Consider useReducer for complex state logic

useEffect

Basic Side Effects

// Effect that runs after every render
useEffect(() => {
document.title = `Count: ${count}`;
});
// Effect that runs only once (componentDidMount)
useEffect(() => {
fetchUserData();
}, []); // Empty dependency array
// Effect with dependencies
useEffect(() => {
if (userId) {
fetchUserData(userId);
}
}, [userId]); // Runs when userId changes
// Effect with cleanup
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(timer); // Cleanup
}, []);

Data Fetching Pattern

function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isCancelled = false;
const fetchUsers = async () => {
try {
setLoading(true);
const response = await fetch('/api/users');
const data = await response.json();
if (!isCancelled) {
setUsers(data);
setError(null);
}
} catch (err) {
if (!isCancelled) {
setError(err.message);
}
} finally {
if (!isCancelled) {
setLoading(false);
}
}
};
fetchUsers();
return () => {
isCancelled = true; // Prevent state updates after unmount
};
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
}))
</ul>
);
}

useContext

Context Setup and Usage

// Create context
const ThemeContext = React.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>
);
}
// Consumer component
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
>
Toggle Theme
</button>
);
}

Additional Hooks

More specialized hooks for advanced state management, performance optimization, and DOM interactions.

useReducer

Complex State Management

// Reducer function
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, {
id: Date.now(),
text: action.payload,
completed: false
}]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
default:
return state;
}
}
// Component using useReducer
function TodoApp() {
const [state, dispatch] = useReducer(todoReducer, {
todos: []
});
const addTodo = (text) => {
dispatch({ type: 'ADD_TODO', payload: text });
};
return /* JSX */;
}

useCallback

Memoizing Functions

function Parent() {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);
// Without useCallback - function recreated on every render
const handleClick = () => {
setCount(count + 1);
};
// With useCallback - function memoized
const handleClickMemoized = useCallback(() => {
setCount(prev => prev + 1);
}, []); // No dependencies needed with functional update
// useCallback with dependencies
const addTodo = useCallback((text) => {
setTodos(prev => [...prev, { id: count, text }]);
}, [count]); // Recreate when count changes
return (
<div>
<ChildComponent onAddTodo={addTodo} />
</div>
);
}
// Child component wrapped with memo
const ChildComponent = React.memo(({ onAddTodo }) => {
console.log('Child rendered'); // Only logs when props change
return <button onClick={() => onAddTodo('New todo')}>Add Todo</button>;
});

Use useCallback when passing functions to optimized child components or as dependencies to other hooks.

useMemo

Memoizing Expensive Calculations

function ExpensiveComponent({ items, filter }) {
// Expensive calculation without memoization
const expensiveValue = items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
).reduce((sum, item) => sum + item.value, 0);
// Memoized expensive calculation
const memoizedValue = useMemo(() => {
console.log('Calculating expensive value...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
).reduce((sum, item) => sum + item.value, 0);
}, [items, filter]); // Only recalculate when dependencies change
// Memoized derived state
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// Memoized object to prevent unnecessary re-renders
const contextValue = useMemo(() => ({
user: { name: 'John', id: 1 },
preferences: { theme: 'dark' }
}), []); // Empty deps - object never changes
return (
<div>
<p>Total: {memoizedValue}</p>
<UserContext.Provider value={contextValue}>
{/* child components */}
</UserContext.Provider>
</div>
);
}

useRef

DOM References and Mutable Values

function RefExamples() {
// DOM reference
const inputRef = useRef(null);
const videoRef = useRef(null);
// Mutable value that persists across renders
const countRef = useRef(0);
const timerRef = useRef(null);
const focusInput = () => {
inputRef.current?.focus();
};
const playVideo = () => {
videoRef.current?.play();
};
// Tracking previous value
const [name, setName] = useState('');
const prevNameRef = useRef();
useEffect(() => {
prevNameRef.current = name;
});
const prevName = prevNameRef.current;
// Interval management
const startTimer = () => {
timerRef.current = setInterval(() => {
countRef.current += 1;
console.log(countRef.current);
}, 1000);
};
const stopTimer = () => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
return (
<div>
<input ref={inputRef} />
<button onClick={focusInput}>Focus Input</button>
<video ref={videoRef} />
<button onClick={playVideo}>Play Video</button>
</div>
);
}

Custom Hooks

Create reusable stateful logic by building your own custom hooks.

Common Patterns

useLocalStorage Hook

function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.log(error);
}
};
return [storedValue, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [username, setUsername] = useLocalStorage('username', '');
return (
<div>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</div>
);
}

useFetch Hook

function useFetch(url, options) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isCancelled = false;
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (!isCancelled) {
setData(result);
}
} catch (err) {
if (!isCancelled) {
setError(err.message);
}
} finally {
if (!isCancelled) {
setLoading(false);
}
}
};
fetchData();
return () => {
isCancelled = true;
};
}, [url, JSON.stringify(options)]);
return { data, loading, error };
}
// Usage
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return <div>{user.name}</div>;
}

Practical Examples

useToggle Hook

function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return [value, toggle, setTrue, setFalse];
}

useDebounce Hook

function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Usage for search
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 300);
useEffect(() => {
if (debouncedSearchTerm) {
// Perform search
console.log('Searching for:', debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}

Best Practices

Guidelines and tips for writing clean, efficient, and maintainable React code with hooks.

Rules of Hooks

🚫 Don't Do This

// ❌ Don't call hooks inside loops, conditions, or nested functions
function BadExample({ items }) {
if (items.length > 0) {
const [count, setCount] = useState(0); // ❌ Conditional hook
}
for (let i = 0; i < items.length; i++) {
const [state, setState] = useState(items[i]); // ❌ Hook in loop
}
const handleClick = () => {
const [clicked, setClicked] = useState(false); // ❌ Hook in function
};
}
Why? React relies on the order of hook calls to track state. Conditional hooks can break this order and cause bugs.

✅ Do This Instead

// ✅ Always call hooks at the top level
function GoodExample({ items }) {
const [count, setCount] = useState(0);
const [clicked, setClicked] = useState(false);
// Use conditions inside the hook logic
useEffect(() => {
if (items.length > 0) {
// Do something with items
}
}, [items]);
const handleClick = () => {
setClicked(true); // ✅ Call state setter, not hook
};
}

Performance Tips

🚀 Performance Optimization

  • useState
    Use functional updates: When new state depends on previous state, use the functional form to avoid stale closures: `setCount(prev => prev + 1)`
  • useEffect
    Optimize dependencies: Include all values from component scope that are used inside the effect. Use ESLint plugin to catch missing dependencies.
  • useCallback
    Memoize callbacks: Use for functions passed to optimized child components or as dependencies to other hooks.
  • useMemo
    Memoize expensive calculations: Only use for computationally expensive operations or to prevent object recreation that causes unnecessary re-renders.
  • React.memo
    Combine with hooks: Wrap components with React.memo and use useCallback/useMemo for props to prevent unnecessary re-renders.

⚠️ Common Pitfalls

  • Overusing useCallback/useMemo: Don't optimize everything. These hooks have overhead and should only be used when necessary.
  • Missing cleanup: Always clean up side effects in useEffect (timers, subscriptions, event listeners) to prevent memory leaks.
  • Stale closures: Be careful with closures in useEffect and useCallback. Include all dependencies or use refs for values that shouldn't trigger re-runs.
  • Infinite loops: Missing dependencies in useEffect can cause infinite re-renders. Use the ESLint plugin to catch these issues.

🎯 Master React Hooks

React Hooks revolutionize how we write components by enabling state and lifecycle features in functional components. Practice building custom hooks to create reusable logic and improve your application's architecture.