React Hooks have revolutionized how we write functional components in React. Introduced in React 16.8, hooks allow you to use state and other React features without writing class components. In this comprehensive guide, we'll explore the most important hooks and how to use them effectively.
React Hooks are functions that allow you to "hook into" React state and lifecycle features from function components. They were introduced to solve several problems:
The useState hook is the most fundamental hook that allows functional components to manage state.
import React, { 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>
)
}
Key points about useState:
The useEffect hook lets you perform side effects in function components. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
import React, { useState, useEffect } from 'react'
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
async function fetchUser() {
setLoading(true)
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>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}
useEffect dependency array:
[]: Effect runs only once (like componentDidMount)The useContext hook lets you consume React context without nesting.
import React, { createContext, useContext, useState } from 'react'
const ThemeContext = createContext()
function App() {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
</ThemeContext.Provider>
)
}
function Header() {
const { theme, setTheme } = useContext(ThemeContext)
return (
<header style={{ background: theme === 'light' ? '#fff' : '#333' }}>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</header>
)
}
For complex state logic, useReducer is often more suitable than useState.
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 are a way to extract component logic into reusable functions.
import { useState, useEffect } from 'react'
// Custom hook for fetching data
function useFetch(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)
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 } = useFetch('/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>
)
}
// ❌ Wrong - hook inside condition
function MyComponent({ condition }) {
if (condition) {
const [state, setState] = useState(false)
}
}
// ✅ Correct - hook at top level
function MyComponent({ condition }) {
const [state, setState] = useState(false)
if (condition) {
// Use state here
}
}
Instead of one large state object, use multiple useState calls for different concerns:
// ❌ Avoid large state objects
const [state, setState] = useState({
name: '',
email: '',
age: 0,
loading: false,
error: null
})
// ✅ Better - separate concerns
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [age, setAge] = useState(0)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
Be careful with the dependency array to avoid infinite loops:
// ❌ May cause infinite loops
useEffect(() => {
fetchData()
}, []) // Missing dependencies
// ✅ Better - include all dependencies
useEffect(() => {
fetchData()
}, [fetchData]) // Include fetchData in dependencies
When passing functions as props, use useCallback to prevent unnecessary re-renders:
import React, { useCallback } from 'react'
function ParentComponent() {
const handleClick = useCallback(() => {
console.log('Button clicked')
}, []) // Empty dependency array for stable reference
return <ChildComponent onClick={handleClick} />
}
React Hooks have made functional components more powerful and easier to work with. They provide a cleaner, more intuitive way to manage state and side effects in React applications.
Key takeaways:
useState for simple state managementuseEffect for side effects and lifecycle managementuseContext for consuming contextuseReducer for complex state logicStart incorporating hooks into your React projects and experience the improved developer experience they provide!
This guide covers the fundamentals of React Hooks. For more advanced patterns and use cases, check out the official React documentation.