Mastering React Hooks: A Complete Guide
React Hooks revolutionized how we write React components by allowing us to use state and other React features in functional components. This guide will take you through the most important hooks and show you how to create your own custom hooks.
What Are React Hooks?
Hooks are functions that let you "hook into" React state and lifecycle features from functional components. They were introduced in React 16.8 and have become the standard way to write React components.
Rules of Hooks
- Only call hooks at the top level of your React functions
- Only call hooks from React functions (components or custom hooks)
- Hook names should start with "use"
useState Hook
The useState
hook lets you add state to functional components:
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
Complex State with useState
function UserProfile() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
})
const updateName = (name: string) => {
setUser(prevUser => ({
...prevUser,
name
}))
}
return (
<div>
<input
value={user.name}
onChange={(e) => updateName(e.target.value)}
placeholder="Name"
/>
<p>Hello, {user.name}!</p>
</div>
)
}
useEffect Hook
The useEffect
hook lets you perform side effects in functional components:
import { useState, useEffect } from 'react'
function DataFetcher() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
async function fetchData() {
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)
}
}
fetchData()
}, []) // Empty dependency array means this runs once
if (loading) return <div>Loading...</div>
return <div>{JSON.stringify(data)}</div>
}
Cleanup with useEffect
function Timer() {
const [seconds, setSeconds] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1)
}, 1000)
// Cleanup function
return () => clearInterval(interval)
}, [])
return <div>Timer: {seconds} seconds</div>
}
useContext Hook
The useContext
hook lets you consume context values:
import { createContext, useContext, useState } from 'react'
// Create context
const ThemeContext = createContext()
// Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
// Consumer component
function ThemedButton() {
const { theme, setTheme } = useContext(ThemeContext)
return (
<button
style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
>
Toggle Theme
</button>
)
}
useReducer Hook
For complex state logic, useReducer
is often preferable to useState
:
import { 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 initialState
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
Custom hooks let you extract component logic into reusable functions:
import { useState, useEffect } from 'react'
function useApi(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
async function fetchData() {
try {
setLoading(true)
const response = await fetch(url)
if (!response.ok) throw new Error('Failed to fetch')
const result = await response.json()
setData(result)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
fetchData()
}, [url])
return { data, loading, error }
}
// Usage
function UserList() {
const { data: users, loading, error } = useApi('/api/users')
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>
)
}
Advanced Hook Patterns
Custom Hook for Local Storage
import { useState, useEffect } from 'react'
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 = (value) => {
try {
setStoredValue(value)
window.localStorage.setItem(key, JSON.stringify(value))
} catch (error) {
console.error('Error saving to localStorage:', error)
}
}
return [storedValue, setValue]
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current theme: {theme}
</button>
)
}
Performance Optimization
useMemo and useCallback
import { useState, useMemo, useCallback } from 'react'
function ExpensiveComponent({ items, onItemClick }) {
// Memoize expensive calculations
const expensiveValue = useMemo(() => {
return items.reduce((sum, item) => sum + item.value, 0)
}, [items])
// Memoize callback functions
const handleClick = useCallback((id) => {
onItemClick(id)
}, [onItemClick])
return (
<div>
<p>Total: {expensiveValue}</p>
{items.map(item => (
<button key={item.id} onClick={() => handleClick(item.id)}>
{item.name}
</button>
))}
</div>
)
}
Best Practices
Hook Best Practices
- Keep hooks at the top level of your components
- Use custom hooks to share logic between components
- Use useCallback and useMemo sparingly, only when you have performance issues
- Prefer multiple useState calls over a single complex state object
- Use useReducer for complex state logic
Testing Hooks
import { renderHook, act } from '@testing-library/react'
import { useCounter } from './useCounter'
test('should increment counter', () => {
const { result } = renderHook(() => useCounter())
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(1)
})
Common Pitfalls
Common Mistakes
- Forgetting dependencies in useEffect - This can cause stale closures
- Calling hooks conditionally - This breaks the rules of hooks
- Not cleaning up effects - This can cause memory leaks
- Overusing useMemo/useCallback - These have their own overhead
Conclusion
React Hooks provide a powerful and flexible way to manage state and side effects in functional components. By mastering these patterns and creating your own custom hooks, you can write more reusable and maintainable React code.
The key to success with hooks is understanding when and how to use each one effectively. Start with the basics (useState and useEffect) and gradually incorporate more advanced patterns as your applications grow in complexity.