React Hooks
useState
Purpose: To store and update reactive data in a component.
When it runs: On every render. The setter from a previous render may schedule a new render when the state value changes, but renders can also be caused by new props, context changes, or parent renders.
Key idea: Changing state triggers a re-render.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Add</button>
</div>
);
}
useEffect
Purpose: To run side effects â anything outside Reactâs render flow.
Examples of side effects:
- Fetching data
- Subscribing to events
- Updating document title
- Setting intervals or timers
When it runs:
After React commits a render. The dependency array controls whether it runs again:
[]â run after the initial render; cleanup still runs on unmount[count]â run after the initial render and again whencountchanges- (no array) â run after every render
import { useEffect, useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Clicked ${count} times`;
}, [count]); // runs only when count changes
return (
<button onClick={() => setCount(count + 1)}>
Click {count}
</button>
);
}
What happens in this example:
- You click â
setCount(count + 1) - React renders the component with the new
count(this is the re-render) - React commits the UI update to the screen
- After the render, React calls your effect â
document.title = ... - If the component re-renders again, React will run the cleanup (if provided) before the next effect, and it also runs cleanup on unmount.
Important:
- Updating
document.titledoes not rerender the UI. - React can rerender because of state setters, new props, context updates, or parent renders.
- Side effects like
document.titledo not trigger re-renders.
Why updating the document title is a side effect (not rendering)?
React only considers something âUI renderingâ if it happens inside the React virtual DOM â meaning React renders components into the screen using its own rendering engine.
For example:
- Changing text in a
<p> - Showing/hiding components
- Updating state that changes JSX
These are all React-managed UI updates. But everything outside Reactâs world is a side effect. React considers anything that touches the browser outside React as a side effect.
Examples:
document.title = "..."window.addEventListener(...)fetch(...)setInterval(...)localStorage.setItem(...)- Mutating the DOM manually (
document.querySelector(...))
These are impure because they affect the world outside the component. So updating the document title is NOT React rendering â itâs modifying the real browser DOM manually, outside Reactâs control.
Summary
| If the update is⊠| Use |
|---|---|
| React-controlled UI (JSX) | useState + rerender |
| Browser or external side effect | useEffect |
| Hook | Purpose | Runs When? |
|---|---|---|
| useState | Store internal state | On every render; setter schedules rerenders when value changes |
| useEffect | Run side effects | After render; reruns based on dependency array and cleans up on unmount |
useMemo - memoize values
Purpose: Avoid expensive recalculations
const filtered = useMemo(() => {
return items.filter(i => i.active);
}, [items]);
Mental Model:
âRecompute this value only when dependencies changeâ
In JavaScript, objects, arrays, and functions are compared by reference, not by value. React uses shallow comparison in many places:
React.memouseEffectdependenciesuseCallback/useMemodependencies
Shallow comparison means:
âAre these the same references as last time?â
Without useMemo,
const filtered = items.filter(i => i.active);
This creates a new array on every render. Even if:
itemsdidnât change- Filter result is identical
filtered is a new reference every time.
How useMemo fixes this:
const filtered = useMemo(() => {
return items.filter(i => i.active);
}, [items]);
Now:
- If
itemsis the same reference - React returns the same
filteredreference
Referential equality is preserved.
Referential equality matters because React does shallow comparisons. useMemo preserves object identity so memoized components and effects donât run unnecessarily.
useCallback â memoize functions
Purpose: Prevent function identity changes
const onClick = useCallback(() => {
setCount(c => c + 1);
}, []);
Mental model:
- Functions are recreated every render
useCallbackkeeps the same function reference
Used with:
React.memo- Dependency-heavy child components
useRef
What is useRef?
const ref = useRef(initialValue);
useRef returns a stable object:
{ current: initialValue }
Two core properties:
ref.currentpersists across renders- Updating
ref.currentdoes NOT trigger a re-render
Mental model:
useRef= a mutable box that React ignores for renderinguseRefis for values React doesnât need to know about
The 3 legitimate use cases of useRef
1. Accessing DOM elements (most common)
const inputRef = useRef<HTMLInputElement>(null);
<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>
Focus
</button>
Why not state?
- DOM nodes donât belong in React state
- You donât want a re-render when the DOM reference changes
2. Storing mutable values across renders (without re-render)
This is the most important conceptual use case.
Example: store previous value
function Counter({ value }: { value: number }) {
const prev = useRef<number | null>(null);
useEffect(() => {
prev.current = value;
}, [value]);
return (
<p>Now: {value}, Before: {prev.current}</p>
);
}
Why useRef works here
- Value survives re-renders
- Updating it doesnât cause another render loop
3. Escape hatch for non-React state
Example: interval ID, timeout ID, external library instance
const intervalId = useRef<number | null>(null);
useEffect(() => {
intervalId.current = window.setInterval(() => {
console.log("tick");
}, 1000);
return () => clearInterval(intervalId.current!);
}, []);