Hooks
advancedPart of Advanced React
Theory
Hooks are functions that let you hook into React state and lifecycle features from functional components. Introduced in React 16.8, they revolutionized how developers write React code by eliminating the need for class components.
What are Hooks?
Before hooks, stateful logic required class components with complex lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount. Hooks simplify all of this into clean, reusable functions.
Common hooks include:
- useState — add state to a component
- useEffect — perform side effects (data fetching, subscriptions, DOM manipulation)
- useRef — hold mutable values that persist across renders
- useMemo — memoize expensive computations
- useCallback — memoize callback functions
Rules of Hooks
Two essential rules govern hooks:
- Only call hooks at the top level. Do not call hooks inside loops, conditions, or nested functions. This ensures that hooks are called in the same order every time a component renders.
// WRONG
if (condition) {
useState(0) // Conditional hook call!
}
// RIGHT
const [value, setValue] = useState(0)- Only call hooks from React functions. Call them from functional components or custom hooks, not from regular JavaScript functions.
These rules exist because React relies on the order of hook calls to correctly associate state with each hook. Breaking the order breaks the component.
useState Deep Dive
useState can hold any type of data: primitives, objects, arrays, or even functions for lazy initialization:
// Lazy initial state — function is only called on the first render
const [data, setData] = useState(() => {
const initial = expensiveComputation()
return initial
})State updates are batched in React 18+. Multiple setState calls within the same event handler are batched into a single re-render:
const [a, setA] = useState(0)
const [b, setB] = useState(0)
// These two updates are batched — only one re-render
setA(prev => prev + 1)
setB(prev => prev + 1)useEffect — Side Effects in React
useEffect lets you perform side effects in your components. It runs after the browser has painted the screen.
useEffect(() => {
// Side effect code here
document.title = `You clicked ${count} times`
}, [count]) // Only re-run when 'count' changesThe dependency array controls when the effect runs:
- No array: Runs after every render
- Empty array
[]: Runs only once (on mount) - With values
[a, b]: Runs whenaorbchange
Cleanup Function
If your effect creates subscriptions, timers, or event listeners, return a cleanup function:
useEffect(() => {
const timer = setInterval(() => {
setTime(new Date())
}, 1000)
return () => clearInterval(timer) // Cleanup on unmount
}, [])Common Use Cases
// Fetching data
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => setUsers(data))
}, [])
// Subscribing to events
useEffect(() => {
const handler = () => console.log('resized')
window.addEventListener('resize', handler)
return () => window.removeEventListener('resize', handler)
}, [])
// Syncing with localStorage
useEffect(() => {
localStorage.setItem('theme', theme)
}, [theme])Custom Hooks
Custom hooks let you extract component logic into reusable functions. A custom hook is a JavaScript function whose name starts with use and that may call other hooks.
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth)
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth)
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return width
}
// Usage in a component:
function MyComponent() {
const width = useWindowWidth()
return <p>Window width: {width}px</p>
}Custom hooks are the primary mechanism for sharing stateful logic between components.
useRef
useRef returns a mutable object that persists across renders without causing re-renders when changed:
function Timer() {
const intervalRef = useRef(null)
const startTimer = () => {
intervalRef.current = setInterval(() => {
console.log('tick')
}, 1000)
}
const stopTimer = () => {
clearInterval(intervalRef.current)
}
return (
<>
<button onClick={startTimer}>Start</button>
<button onClick={stopTimer}>Stop</button>
</>
)
}Refs are commonly used to access DOM elements directly:
function TextInput() {
const inputRef = useRef(null)
const focusInput = () => {
inputRef.current.focus()
}
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
</>
)
}useMemo and useCallback
useMemo memoizes the result of an expensive computation:
const sortedList = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name))
}, [items])useCallback memoizes a function definition:
const handleClick = useCallback(() => {
console.log('Clicked!')
}, []) // Same function instance across rendersThese are optimization tools. Use them only when you have measured performance issues.
Don't wrap everything in useMemo or useCallback. Premature optimization can make your code harder to read. Measure first, optimize second.
Practical Examples
Use useRef when you need to store a value that should persist across renders but shouldn't trigger a re-render when it changes. Refs are perfect for timers, DOM references, and tracking mutable values.
Exercises
Build a useLocalStorage Hook
Create a custom hook called useLocalStorage that works like useState but persists the value to localStorage. It should accept a key and an initial value. The hook should read from localStorage on initialization and write to localStorage on every update.
Expected Output:
The input value is persisted to localStorage. Refreshing the page restores the saved value.useEffect with Cleanup
Create a MouseTracker component that displays the current mouse position (X, Y coordinates). Add a toggle button to start and stop tracking. Use useEffect with proper cleanup to remove the event listener when tracking is off.
Expected Output:
A page that shows mouse coordinates when tracking is enabled and stops updating when tracking is disabledCreate a useDebounce Hook
Create a custom hook called useDebounce that debounces a value. It should accept the value and a delay in milliseconds. The debounced value should only update after the specified delay has passed since the last change.
Expected Output:
The console log only fires 500ms after the user stops typing. Rapid typing should not trigger multiple requests.Mini Quiz
Mini Quiz
Mini Project
Mini Project: Real-Time Search with Debounce
Build a product search page that fetches results from a mock API. Use custom hooks (useDebounce, useFetch), useEffect for data fetching with cleanup, and useRef for managing the search input focus.
Requirements:
Bonus Challenge
Add highlighted search terms in the results. Also add a 'Clear' button that resets the input and refocuses it.