React Hooks: A Deep Dive into Modern React Development
React Hooks were introduced in React 16.8 and have fundamentally changed how we write React applications. They allow functional components to have state and side effects, making them as powerful as class components.
Hooks are functions that let you “hook into” React state and lifecycle features from function components. They must be called at the top level of your component and cannot be called inside loops, conditions, or nested functions.
The useState hook is the most fundamental hook for managing state in functional components:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
</div>
);
}useState calls for unrelated state// Functional update
const [count, setCount] = useState(0);
setCount(prevCount => prevCount + 1);
// Lazy initialization
const [data, setData] = useState(() => {
const savedData = localStorage.getItem('data');
return savedData ? JSON.parse(savedData) : [];
});The useEffect hook handles side effects in functional components:
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('Error fetching user:', error);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // Dependency array
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}The dependency array controls when the effect runs:
// Runs after every render
useEffect(() => {
console.log('Component rendered');
});
// Runs only on mount
useEffect(() => {
console.log('Component mounted');
}, []);
// Runs when dependencies change
useEffect(() => {
console.log('User ID changed:', userId);
}, [userId]);The useContext hook provides a way to pass data through the component tree without manually passing props:
// Create context
const ThemeContext = React.createContext();
// Provider component
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
</ThemeContext.Provider>
);
}
// Consumer component
function Header() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<header className={`header-${theme}`}>
<h1>My App</h1>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</header>
);
}The useReducer hook is useful for managing complex state logic:
import React, { useReducer } from 'react';
const initialState = { count: 0 };
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, initialState);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>
+
</button>
<button onClick={() => dispatch({ type: 'decrement' })}>
-
</button>
<button onClick={() => dispatch({ type: 'reset' })}>
Reset
</button>
</div>
);
}Custom hooks allow you to extract component logic into reusable functions:
// Custom hook for API calls
function 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 };
}
// Using the custom hook
function UserList() {
const { data: users, loading, error } = useApi('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}These hooks help optimize performance by memoizing values and functions:
import React, { useState, useMemo, useCallback } from 'react';
function ExpensiveComponent({ items, filter }) {
// Memoize expensive calculation
const filteredItems = useMemo(() => {
return items.filter(item => item.includes(filter));
}, [items, filter]);
// Memoize callback function
const handleItemClick = useCallback((item) => {
console.log('Item clicked:', item);
}, []);
return (
<ul>
{filteredItems.map(item => (
<li key={item} onClick={() => handleItemClick(item)}>
{item}
</li>
))}
</ul>
);
}useState for simple stateuseReducer for complex state logicuseMemo for expensive calculationsuseCallback for function propsuseEffect(() => {
const subscription = someAPI.subscribe();
// Cleanup function
return () => {
subscription.unsubscribe();
};
}, []);function useForm(initialState) {
const [values, setValues] = useState(initialState);
const handleChange = useCallback((e) => {
const { name, value } = e.target;
setValues(prev => ({
...prev,
[name]: value
}));
}, []);
const reset = useCallback(() => {
setValues(initialState);
}, [initialState]);
return { values, handleChange, reset };
}function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = useCallback((value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Error saving to localStorage:', error);
}
}, [key]);
return [storedValue, setValue];
}React Hooks have made functional components more powerful and easier to work with. By mastering these hooks and following best practices, you can build more maintainable and performant React applications.
Remember to always consider the dependency array in useEffect, use custom hooks to extract reusable logic, and optimize performance with useMemo and useCallback when necessary.