More Hooks
useContext, useRef, useMemo, useCallback and custom hooks
Beyond useState and useEffect
React provides several other built-in hooks for specific use cases. These hooks help you manage context, references, memoization, and more. You can also create your own custom hooks!
useRef
Creates a mutable reference that persists across renders without causing re-renders:
import { useRef, useEffect } from 'react';
// Accessing DOM elements
function FocusInput() {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>Focus Input</button>
</div>
);
}
// Storing mutable values (doesn't trigger re-render)
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
const start = () => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
};
return (
<div>
<p>{count}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
// Previous value pattern
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
});
return (
<p>
Current: {count}, Previous: {prevCountRef.current}
</p>
);
}
useContext
Access context values without prop drilling:
import { createContext, useContext, useState } from 'react';
// 1. Create context
const ThemeContext = createContext('light');
// 2. Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(t => t === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 3. Use context in any child component
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
style={{
background: theme === 'dark' ? '#333' : '#fff',
color: theme === 'dark' ? '#fff' : '#333'
}}
>
Toggle Theme ({theme})
</button>
);
}
// 4. Wrap app with provider
function App() {
return (
<ThemeProvider>
<Header />
<ThemedButton />
<Footer />
</ThemeProvider>
);
}
useMemo
Memoize expensive calculations to avoid re-computing on every render:
import { useMemo, useState } from 'react';
function ExpensiveList({ items, filter }) {
// Only recalculates when items or filter changes
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
// Complex calculation example
function Fibonacci({ n }) {
const result = useMemo(() => {
const fib = (num) => {
if (num <= 1) return num;
return fib(num - 1) + fib(num - 2);
};
return fib(n);
}, [n]);
return <p>Fibonacci({n}) = {result}</p>;
}
⚠️ Don't Overuse
Only use useMemo for truly expensive calculations. Premature optimization can make code harder to read without real benefits.
useCallback
Memoize functions to maintain referential equality across renders:
import { useCallback, useState, memo } from 'react';
// Child component wrapped in memo
const ExpensiveChild = memo(function ExpensiveChild({ onClick }) {
console.log('Child rendered');
return <button onClick={onClick}>Click me</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// Without useCallback: new function on every render
// const handleClick = () => console.log('Clicked!');
// With useCallback: same function reference
const handleClick = useCallback(() => {
console.log('Clicked!');
}, []); // Empty deps = never changes
// With dependencies
const handleSubmit = useCallback(() => {
console.log('Submitting:', name);
}, [name]); // New function only when name changes
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<ExpensiveChild onClick={handleClick} />
</div>
);
}
useReducer
Alternative to useState for complex state logic:
import { useReducer } from 'react';
// Reducer function
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 };
case 'set':
return { count: action.payload };
default:
throw new Error('Unknown action');
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
<button onClick={() => dispatch({ type: 'set', payload: 100 })}>
Set to 100
</button>
</div>
);
}
Custom Hooks
Extract reusable logic into custom hooks:
// Custom hook for local storage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>{theme}</button>;
}
// Custom hook for fetch
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// Usage
function UserProfile({ id }) {
const { data: user, loading, error } = useFetch(`/api/users/${id}`);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error!</p>;
return <h1>{user.name}</h1>;
}
// Custom hook for window size
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// Usage
function ResponsiveComponent() {
const { width } = useWindowSize();
return <p>Width: {width}px</p>;
}
Hooks Rules
Rules of Hooks
- 1. Only call hooks at the top level (not inside loops, conditions, or nested functions)
- 2. Only call hooks from React functions (components or custom hooks)
- 3. Custom hooks must start with "use" (useLocalStorage, useFetch)
🎯 Hooks Best Practices
- ✓ Use useRef for DOM access and mutable values that don't need re-renders
- ✓ Use useContext to avoid prop drilling
- ✓ Use useMemo for expensive calculations only
- ✓ Use useCallback when passing callbacks to memoized children
- ✓ Use useReducer for complex state with multiple actions
- ✓ Extract shared logic into custom hooks
- ✓ Name custom hooks starting with "use"