| Concept | Key API |
|---|---|
| State | useState / useReducer |
| Side effects | useEffect / useLayoutEffect |
| Context | createContext / useContext |
| Refs | useRef / forwardRef |
| Memoisation | useMemo / useCallback /
memo |
| Portals | createPortal |
| Custom hook | function useX() {} |
| Event handling | onClick, onChange, etc. |
| Form values | event.target.value |
| Persisted ref | event.persist() (classic) |
const [count, setCount] = useState(0);
const [user, setUser] = useState({ name: "", age: 0 });
setCount(42); // Replace value
setCount(c => c + 1); // Functional update
setUser(prev => ({ ...prev, name: "Max" }));
useEffect(() => {
// Run on mount + every render
});
useEffect(() => {
// Run on mount only
}, []);
useEffect(() => {
// Run when count changes
return () => { /* cleanup */ };
}, [count]);
useEffect(() => {
const id = setInterval(() => {}, 1000);
return () => clearInterval(id); // cleanup on unmount
}, []);
// Fires synchronously after DOM mutations but before paint.
// Use when you need to measure layout or avoid visual flicker.
useLayoutEffect(() => {
ref.current.scrollTop = 0;
}, [messages]);
const reducer = (state, action) => {
switch (action.type) {
case "inc": return { count: state.count + 1 };
case "dec": return { count: state.count - 1 };
default: return state;
}
};
const [state, dispatch] = useReducer(reducer, { count: 0 });
dispatch({ type: "inc" });
const ThemeCtx = createContext("light");
const theme = useContext(ThemeCtx);
const inputRef = useRef(null);
const renderCount = useRef(0);
renderCount.current += 1;
// Attach to DOM element
<input ref={inputRef} />
// Read later
inputRef.current.focus();
// Recomputes only when deps change
const sorted = useMemo(() => {
return items.sort((a, b) =>
a.name.localeCompare(b.name));
}, [items]);
// Stable function reference across renders
const handleClick = useCallback((id) => {
setSelected(id);
}, []);
// Expose a custom API to parent via ref
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
reset: () => setValue(""),
}));
// Stable unique ID for accessibility (React 18+)
const id = useId();
<label htmlFor={id}>Name</label>
<input id={id} />
const deferred = useDeferredValue(searchTerm);
const [isPending, startTransition] = useTransition();
startTransition(() => setSearchTerm(value));
// Labels custom hooks in React DevTools
useDebugValue(isOnline ? "Online" : "Offline");
use
(e.g. useWindowSize).React wraps native browser events in a cross-browser
SyntheticEvent.
<button onClick={(e) => handleClick(e)}>Click</button>
<input onChange={(e) => setName(e.target.value)} />
<form onSubmit={(e) => { e.preventDefault(); submit(); }}>
e.target // Element that triggered the event
e.currentTarget // Element the handler is attached to
e.preventDefault() // Cancel default behaviour
e.stopPropagation()// Stop bubbling
e.nativeEvent // Underlying browser event (rarely needed)
e.type // e.g. "click", "keydown"
<input
onKeyDown={(e) => {
if (e.key === "Escape") close();
if (e.key === "Enter") submit();
if (e.ctrlKey && e.key === "s") save();
}}
/>
// onFocus / onBlur bubble (React 17+ uses native
// focusin/focusout)
<div onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}>
<input />
</div>
onMouseEnter / onMouseLeave // Don't bubble from children
onMouseOver / onMouseOut // Do bubble
onPointerDown / onPointerUp // Unified mouse + touch + pen
const ThemeCtx = createContext("light"); // default value
function App() {
return (
<ThemeCtx.Provider value="dark">
<Toolbar />
</ThemeCtx.Provider>
);
}
// Hook (preferred)
const theme = useContext(ThemeCtx);
// Render-prop (class components)
<ThemeCtx.Consumer>
{theme => <div className={theme}>...</div>}
</ThemeCtx.Consumer>
const UserCtx = createContext(null);
const SetUserCtx = createContext(null);
function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
<UserCtx.Provider value={user}>
<SetUserCtx.Provider value={setUser}>
{children}
</SetUserCtx.Provider>
</UserCtx.Provider>
);
}
// Memoise the value object to avoid re-rendering
// all consumers
const value = useMemo(() => ({ user, setUser }), [user]);
<UserCtx.Provider value={value}>...</UserCtx.Provider>
// Shallow-compare props; skip re-render if unchanged
const Greeting = React.memo(function Greeting({ name }) {
return <h1>Hello {name}</h1>;
});
// Custom comparison
const Greeting = React.memo(
function Greeting({ user }) {
return <h1>{user.name}</h1>;
},
(prev, next) => prev.user.id === next.user.id
);
// Avoid recomputing expensive calculations
const visibleItems = useMemo(
() => items.filter(i => i.score > threshold),
[items, threshold]
);
// Stable callback — essential when passing to memoised
// children
const onDelete = useCallback((id) => {
setItems(prev => prev.filter(i => i.id !== id));
}, []);
memo — child component receives same props
frequently.useMemo — expensive computation with stable
deps.useCallback — callback passed to
memo-wrapped child.const inputRef = useRef(null);
<input ref={inputRef} />
// Focus imperatively
inputRef.current?.focus();
// Measure layout
const rect = inputRef.current?.getBoundingClientRect();
const prevValue = useRef(null);
useEffect(() => {
prevValue.current = value;
});
const changed = prevValue.current !== value;
const FancyInput = forwardRef((props, ref) => {
return <input ref={ref} className="fancy"
{...props} />;
});
// Parent
const ref = useRef(null);
<FancyInput ref={ref} />;
ref.current.focus();
// Called when the ref is attached or detached
<div ref={(node) => {
if (node) {
node.scrollTop = node.scrollHeight;
// measure, attach listeners, etc.
}
}} />
<div ref={(node) => {
if (node) {
const observer = new ResizeObserver(callback);
observer.observe(node);
return () => observer.disconnect(); // cleanup
}
}} />
Portals render children into a DOM node outside the parent's hierarchy while preserving React's event bubbling.
import { createPortal } from "react-dom";
function Modal({ children }) {
return createPortal(
<div className="modal-overlay">{children}</div>,
document.getElementById("portal-root")
// or document.body
);
}
// Events still bubble through the React tree, NOT the
// DOM tree. A click inside the portal still triggers
// onClick on the parent component in React, even though
// the DOM is elsewhere.
function App() {
return (
<div onClick={() => console.log("bubbled!")}>
<Modal>
<button>Click me</button>
</Modal>
</div>
);
}
overflow: hidden clipping)// Fetch wrapper
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
fetch(url).then(r => r.json()).then(d => {
if (!cancelled) { setData(d); setLoading(false); }
});
return () => { cancelled = true; };
}, [url]);
return { data, loading };
}
// Window size
function useWindowSize() {
const [size, setSize] = useState({
w: window.innerWidth, h: window.innerHeight
});
useEffect(() => {
const handler = () => setSize({
w: window.innerWidth, h: window.innerHeight
});
window.addEventListener("resize", handler);
return () =>
window.removeEventListener("resize", handler);
}, []);
return size;
}
<StrictMode>)
double-invokes effects in dev to surface bugs. Don't disable it — fix
the cleanup.index when the list can reorder.value + onChange) over uncontrolled unless
you have a specific reason.useState(() => expensive()) avoids recomputing on every
render.memo or
useMemo.