React Hooks revolutionized how we write React components. They allow you to use state and other React features in functional components, making your code cleaner and more reusable.
The useState hook lets you add state to functional components.
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(count - 1)}>
Decrement
</button>
</div>
);
};
const UserProfile = () => {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
const updateName = (newName) => {
setUser(prevUser => ({
...prevUser,
name: newName
}));
};
return (
<div>
<input
value={user.name}
onChange={(e) => updateName(e.target.value)}
placeholder="Enter name"
/>
<p>Hello, {user.name}!</p>
</div>
);
};
useEffect lets you perform side effects in functional components. It combines the functionality of componentDidMount, componentDidUpdate, and componentWillUnmount.
import React, { useState, useEffect } from 'react';
const DataFetcher = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// This runs after every render
fetchData();
}, []); // Empty dependency array = run once on mount
const fetchData = async () => {
try {
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setLoading(false);
}
};
if (loading) return <div>Loading...</div>;
return (
<div>
<h2>Data:</h2>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};
The dependency array controls when the effect runs:
const SearchComponent = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
// Runs when query changes
useEffect(() => {
if (query) {
searchAPI(query).then(setResults);
}
}, [query]); // Runs when query changes
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
{/* Render results */}
</div>
);
};
Some effects need cleanup to prevent memory leaks:
const TimerComponent = () => {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// Cleanup function
return () => clearInterval(interval);
}, []);
return <div>Timer: {seconds}s</div>;
};
useContext provides a way to share values between components without prop drilling.
import React, { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
const ThemedButton = () => {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
className={theme === 'light' ? 'light-theme' : 'dark-theme'}
onClick={toggleTheme}
>
Switch to {theme === 'light' ? 'dark' : 'light'} theme
</button>
);
};
For complex state logic, useReducer is often better than useState:
import React, { useReducer } from 'react';
const 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;
}
};
const TodoApp = () => {
const [todos, dispatch] = useReducer(todoReducer, []);
const [inputText, setInputText] = useState('');
const addTodo = () => {
if (inputText.trim()) {
dispatch({ type: 'ADD_TODO', text: inputText });
setInputText('');
}
};
return (
<div>
<input
value={inputText}
onChange={(e) => setInputText(e.target.value)}
/>
<button onClick={addTodo}>Add Todo</button>
{todos.map(todo => (
<div key={todo.id}>
<span
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
onClick={() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })}
>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'DELETE_TODO', id: todo.id })}>
Delete
</button>
</div>
))}
</div>
);
};
Create reusable stateful logic with custom hooks:
// Custom hook for API calls
const useApi = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
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
const UserProfile = ({ userId }) => {
const { data: user, loading, error } = useApi(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>Welcome, {user.name}!</div>;
};
useEffect should handle one concernuseState for simple state, useReducer for complex state logicUnderstanding hooks deeply will make you a more effective React developer. Practice with different scenarios and gradually build more complex applications!