React
August 15, 2025 12 min readMastering React Hooks: A Complete Guide Deep dive into React Hooks and learn how to use useState, useEffect, useContext, and custom hooks to build powerful React applications.
MN Makara Nuol
Intermediate
React
Hooks
JavaScript
Frontend
State Management
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:
Copy code 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
Copy code 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:
Copy code 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
Copy code 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:
Copy code 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:
Copy code 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:
Custom Hook for API Calls Copy code 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
Copy code 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
Copy code 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
Copy code 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.