React Hooks: useState, useEffect, useContext
React Hooks enable state management and side effects in functional components. This article covers essential hooks and patterns.
Introduction
React Hooks provide:
- State management
- Side effect handling
- Context consumption
- Code reusability
- Cleaner components
Understanding hooks helps you:
- Manage component state
- Handle side effects
- Share state across components
- Create reusable logic
- Write cleaner code
useState Hook
Basic useState
import { useState } from 'react';
// โ
Good: Basic state
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// โ
Good: Multiple state variables
function Form() {
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>
);
}
// โ
Good: State with objects
function User() {
const [user, setUser] = useState({
name: 'John',
age: 30,
email: '[email protected]'
});
const updateName = (newName) => {
setUser({ ...user, name: newName });
};
return <div>{user.name}</div>;
}
// โ
Good: 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>
);
}
Lazy Initialization
// โ
Good: Lazy initialization
function ExpensiveComponent() {
const [state, setState] = useState(() => {
// This runs only once on mount
const initialState = expensiveCalculation();
return initialState;
});
return <div>{state}</div>;
}
// โ
Good: Functional updates
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
// Use functional update for dependent updates
setCount(prevCount => prevCount + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
useEffect Hook
Basic useEffect
import { useEffect, useState } from 'react';
// โ
Good: Run effect on mount
function Component() {
useEffect(() => {
console.log('Component mounted');
}, []);
return <div>Component</div>;
}
// โ
Good: Run effect on dependency change
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return <div>{user?.name}</div>;
}
// โ
Good: Cleanup function
function Timer() {
useEffect(() => {
const interval = setInterval(() => {
console.log('Tick');
}, 1000);
// Cleanup function
return () => clearInterval(interval);
}, []);
return <div>Timer</div>;
}
// โ
Good: Multiple effects
function Component() {
useEffect(() => {
console.log('Effect 1');
}, []);
useEffect(() => {
console.log('Effect 2');
}, []);
return <div>Component</div>;
}
Common Patterns
// โ
Good: Fetch data on mount
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUsers = async () => {
try {
const response = await fetch('/api/users');
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// โ
Good: Debounced search
function SearchUsers() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
const timer = setTimeout(() => {
if (query) {
searchUsers(query).then(setResults);
}
}, 500);
return () => clearTimeout(timer);
}, [query]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search users"
/>
<ul>
{results.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
// โ
Good: Event listener
function WindowSize() {
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);
}, []);
return <div>Width: {size.width}, Height: {size.height}</div>;
}
useContext Hook
Basic useContext
import { createContext, useContext } from 'react';
// โ
Good: Create context
const ThemeContext = createContext();
// โ
Good: 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>
);
}
// โ
Good: Use context
function ThemedComponent() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<div style={{ background: theme === 'light' ? '#fff' : '#000' }}>
<p>Current theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
}
// โ
Good: App setup
function App() {
return (
<ThemeProvider>
<ThemedComponent />
</ThemeProvider>
);
}
Multiple Contexts
// โ
Good: Multiple contexts
const AuthContext = createContext();
const NotificationContext = createContext();
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
return (
<AuthContext.Provider value={{ user, setUser }}>
{children}
</AuthContext.Provider>
);
}
function NotificationProvider({ children }) {
const [notifications, setNotifications] = useState([]);
const addNotification = (message) => {
setNotifications([...notifications, message]);
};
return (
<NotificationContext.Provider value={{ notifications, addNotification }}>
{children}
</NotificationContext.Provider>
);
}
// โ
Good: Use multiple contexts
function Dashboard() {
const { user } = useContext(AuthContext);
const { notifications } = useContext(NotificationContext);
return (
<div>
<h1>Welcome, {user?.name}</h1>
<p>Notifications: {notifications.length}</p>
</div>
);
}
// โ
Good: App with multiple providers
function App() {
return (
<AuthProvider>
<NotificationProvider>
<Dashboard />
</NotificationProvider>
</AuthProvider>
);
}
Custom Hooks
Creating Custom Hooks
// โ
Good: Custom hook for form handling
function useForm(initialValues, onSubmit) {
const [values, setValues] = useState(initialValues);
const handleChange = (e) => {
const { name, value } = e.target;
setValues({ ...values, [name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(values);
};
const reset = () => {
setValues(initialValues);
};
return { values, handleChange, handleSubmit, reset };
}
// Usage
function LoginForm() {
const { values, handleChange, handleSubmit } = useForm(
{ email: '', password: '' },
(values) => console.log('Submit:', values)
);
return (
<form onSubmit={handleSubmit}>
<input
name="email"
value={values.email}
onChange={handleChange}
/>
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
/>
<button type="submit">Login</button>
</form>
);
}
// โ
Good: Custom hook for API calls
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// Usage
function UserList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// โ
Good: Custom hook for local storage
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// Usage
function Preferences() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<div>
<p>Theme: {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</div>
);
}
Best Practices
-
Use hooks at top level:
// โ Good function Component() { const [state, setState] = useState(0); return <div>{state}</div>; } // โ Bad function Component() { if (condition) { const [state, setState] = useState(0); } } -
Use functional updates:
// โ Good setCount(prevCount => prevCount + 1); // โ Bad setCount(count + 1); -
Clean up effects:
// โ Good useEffect(() => { const timer = setInterval(() => {}, 1000); return () => clearInterval(timer); }, []); // โ Bad useEffect(() => { setInterval(() => {}, 1000); }, []);
Summary
React Hooks are essential. Key takeaways:
- Use useState for state management
- Use useEffect for side effects
- Use useContext for global state
- Create custom hooks for reusable logic
- Clean up effects properly
- Use functional updates
- Follow hook rules
Related Resources
Next Steps
- Learn about React Component Patterns
- Explore State Management
- Study React Router
- Practice hooks
- Build React applications
Comments