React Hooks Deep Dive
15 min read
React Hooks Deep Dive
React Hooks revolutionized how we write React components. Let's explore hooks from basics to advanced patterns.
Understanding useState
Basic Usage
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
)
}
Functional Updates
function Counter() {
const [count, setCount] = useState(0)
const increment = () => {
// Use functional update when new state depends on previous state
setCount(prevCount => prevCount + 1)
}
const incrementTwice = () => {
// This ensures both increments happen
setCount(prev => prev + 1)
setCount(prev => prev + 1)
}
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={incrementTwice}>Increment Twice</button>
</div>
)
}
useEffect Patterns
Cleanup and Dependencies
import { useState, useEffect } from 'react'
function Timer() {
const [seconds, setSeconds] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1)
}, 1000)
// Cleanup function
return () => clearInterval(interval)
}, []) // Empty dependency array = run once on mount
return <div>Seconds: {seconds}</div>
}
Data Fetching
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
async function fetchUser() {
try {
setLoading(true)
const response = await fetch(`/api/users/${userId}`)
const userData = await response.json()
if (!cancelled) {
setUser(userData)
}
} catch (err) {
if (!cancelled) {
setError(err.message)
}
} finally {
if (!cancelled) {
setLoading(false)
}
}
}
fetchUser()
return () => {
cancelled = true
}
}, [userId])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
if (!user) return <div>User not found</div>
return <div>Hello, {user.name}!</div>
}
Advanced Hooks
useReducer for Complex State
import { useReducer } from 'react'
const initialState = {
items: [],
loading: false,
error: null
}
function reducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null }
case 'FETCH_SUCCESS':
return { ...state, loading: false, items: action.payload }
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload }
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] }
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload)
}
default:
return state
}
}
function ItemList() {
const [state, dispatch] = useReducer(reducer, initialState)
const fetchItems = async () => {
dispatch({ type: 'FETCH_START' })
try {
const response = await fetch('/api/items')
const items = await response.json()
dispatch({ type: 'FETCH_SUCCESS', payload: items })
} catch (error) {
dispatch({ type: 'FETCH_ERROR', payload: error.message })
}
}
return (
<div>
<button onClick={fetchItems}>Fetch Items</button>
{state.loading && <div>Loading...</div>}
{state.error && <div>Error: {state.error}</div>}
<ul>
{state.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
)
}
useCallback and useMemo
import { useState, useCallback, useMemo } from 'react'
function ExpensiveComponent({ items, onItemClick }) {
// Memoize expensive calculations
const expensiveValue = useMemo(() => {
return items.reduce((sum, item) => sum + item.value, 0)
}, [items])
// Memoize event handlers
const handleClick = useCallback((item) => {
onItemClick(item.id)
}, [onItemClick])
return (
<div>
<p>Total Value: {expensiveValue}</p>
{items.map(item => (
<button
key={item.id}
onClick={() => handleClick(item)}
>
{item.name}
</button>
))}
</div>
)
}
Custom Hooks
useLocalStorage Hook
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) {
console.error('Error reading from localStorage:', 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 saving to localStorage:', error)
}
}
return [storedValue, setValue]
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
return (
<div>
<p>Current theme: {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</div>
)
}
useApi Hook
import { useState, useEffect, useCallback } from 'react'
function useApi(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const fetchData = useCallback(async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result = await response.json()
setData(result)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}, [url])
useEffect(() => {
fetchData()
}, [fetchData])
const refetch = useCallback(() => {
fetchData()
}, [fetchData])
return { data, loading, error, refetch }
}
// Usage
function UserList() {
const { data: users, loading, error, refetch } = useApi('/api/users')
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
return (
<div>
<button onClick={refetch}>Refresh</button>
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}
Hook Rules and Best Practices
Rules of Hooks
- Only call hooks at the top level
- Only call hooks from React functions
- Use ESLint plugin for enforcement
// ❌ Wrong - conditional hook
function BadComponent({ condition }) {
if (condition) {
const [state, setState] = useState(0) // This breaks the rules!
}
}
// ✅ Correct - hook at top level
function GoodComponent({ condition }) {
const [state, setState] = useState(0)
if (condition) {
// Use the state conditionally instead
return <div>{state}</div>
}
return null
}
Performance Optimization
import { memo, useCallback, useMemo } from 'react'
const ExpensiveChild = memo(({ data, onUpdate }) => {
// This component only re-renders when data or onUpdate changes
return <div>{data.name}</div>
})
function Parent() {
const [count, setCount] = useState(0)
const [items, setItems] = useState([])
// Memoize callback to prevent unnecessary re-renders
const handleUpdate = useCallback((id) => {
setItems(prev => prev.map(item =>
item.id === id ? { ...item, updated: true } : item
))
}, [])
// Memoize expensive computation
const processedData = useMemo(() => {
return items.map(item => ({
...item,
processed: true
}))
}, [items])
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
{processedData.map(item => (
<ExpensiveChild
key={item.id}
data={item}
onUpdate={handleUpdate}
/>
))}
</div>
)
}
Conclusion
React Hooks provide a powerful and flexible way to manage state and side effects in functional components. Master the built-in hooks, understand their patterns, and create custom hooks to encapsulate and reuse stateful logic across your application.