Note Composition via props is preferred over inheritance. Passing components as props gives you flexible "slot" patterns without tightly coupling parent and child.
function Notification({ message, type }) {
if (!message) returnnull;
return (
<div className={`alert alert-${type}`}>
{type === "error" ? "Error: " : "Info: "}
{message}
</div>
);
}
output
Returns null when no message. Otherwise renders an alert with type prefix.
Note Returning null renders nothing. Watch out with &&: if the left side is 0, React renders "0" because it is a valid JSX child. Use Boolean(count) && <Component /> or count > 0 && <Component /> instead.
Note Keys must be stable, unique among siblings, and derived from data (like a database ID). Never use array index as key if the list order can change -- it causes subtle rendering bugs and broken state.
render listmap arraylist keyrender arraydynamic list
Fragments
syntax
<React.Fragment>...</React.Fragment>
// Short syntax
<>...</>
Note Fragments let you group elements without adding extra DOM nodes. Use the long syntax <React.Fragment key={...}> when mapping a list, since the short syntax does not support the key attribute.
fragmentgroup elementsno wrapper divmultiple elementsadjacent elements
Note JSX is syntactic sugar for function calls. Every tag must be closed. Use className instead of class, htmlFor instead of for. Attribute names follow camelCase convention.
jsx syntaxjsx rulesjsx basicswrite jsx
Embedding Expressions
syntax
{expression}
// Any valid JavaScript expression inside curly braces
Note You can embed any JavaScript expression inside curly braces: variables, function calls, math, ternaries. Statements (if, for, while) cannot appear inside JSX directly -- use them before the return.
jsx expressionembed javascriptcurly bracesdynamic content in jsx
Note For more than two branches, extract the logic into a variable or helper function above the return statement rather than nesting ternaries. Readable code wins over clever one-liners.
function FormField({ label, ...inputProps }) {
return (
<div className="field">
<label>{label}</label>
<input {...inputProps} />
</div>
);
}
// Usage: all standard input attributes are forwarded
<FormField
label="Email"type="email"
placeholder="[email protected]"
required
/>
Note Spread is great for forwarding props. Be careful: it can pass unintended attributes to DOM elements. Destructure known props first, then spread the rest.
spread propsforward propspass all propsrest propsproxy component
// This JSX:const element = (
<div className="container">
<h1>Title</h1>
<p>Paragraph</p>
</div>
);
// Compiles to:const element = React.createElement(
"div",
{ className: "container" },
React.createElement("h1", null, "Title"),
React.createElement("p", null, "Paragraph")
);
Note You almost never need to call createElement directly. JSX is syntactically cleaner and the standard in the ecosystem. The JSX transform in modern setups does not require importing React at the top of every file.
jsx createElementjsx compilationwithout jsxhow jsx works
Note Style properties use camelCase (backgroundColor, not background-color). Values that need units must include them as strings ("8px"), except for unitless properties like opacity and zIndex which accept numbers.
Displays count starting at 0. Clicking increment adds 1 each time.
Note setState triggers a re-render. The state value remains the same within the current render cycle even after calling setState -- the new value appears on the next render.
useStatemanage statecomponent statestate variablebasic state
Updater Function
syntax
setState(prevState => newState);
example
function StepCounter() {
const [count, setCount] = useState(0);
function incrementThree() {
// Each updater receives the latest pending state
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
}
return (
<button onClick={incrementThree}>
Count: {count}
</button>
);
}
output
Each click adds 3 to the count, because each updater builds on the previous pending value.
Note Always use the updater form when the next state depends on the previous state. Writing setCount(count + 1) three times in a row would only add 1 because count is a stale snapshot from the current render.
updater functionprevious statefunctional updateincrement statestate based on previous
function FilteredList() {
// Expensive computation runs only on mount, not every renderconst [sortedItems, setSortedItems] = useState(() => {
const saved = localStorage.getItem('cachedItems');
return saved ? JSON.parse(saved) : [];
});
return <ItemList items={sortedItems} />;
}
Note Pass a function (not the result of calling it) to avoid running expensive initialization logic on every render. React only calls the initializer function during the first render and ignores it on subsequent renders.
lazy initializationexpensive initial stateinitial state functionlocalStorage state
Note Always spread the previous object to create a new copy. Directly mutating state (profile.firstName = 'X') will NOT trigger a re-render because React compares by reference.
Note Use filter to remove, map to update, and spread to add. Never use push, splice, or direct index assignment on state arrays -- those mutate the existing array instead of creating a new one.
array stateadd to arrayremove from arrayupdate array itemtodo list state
Note Separate state variables are preferred when values change independently. Combine into an object only when values logically belong together and always change at the same time.
multiple stateseparate state variablesform statewhen to combine state
Note The effect runs after the component renders. The dependency array controls when it re-runs. Omit the array: runs after every render. Empty array []: runs once on mount. With values: runs when any dependency changes.
useEffectside effectafter renderlifecyclecomponent mount
Dependency Array
syntax
useEffect(effectFn, [dep1, dep2]);
// Empty array: mount only
useEffect(effectFn, []);
// No array: every render
useEffect(effectFn);
Note React uses Object.is to compare dependencies. Objects and arrays create new references each render, so they will always trigger the effect. Extract primitive values or memoize objects before listing them as dependencies.
Note The cleanup function runs before the effect re-executes (when dependencies change) and when the component unmounts. This prevents memory leaks from subscriptions, timers, and open connections.
Note The ignore flag prevents setting state on an unmounted component or after a stale request. In React 19, consider using the use() hook with Suspense for data fetching instead of this pattern.
Note Always remove event listeners in the cleanup to avoid duplicate handlers and memory leaks. The handler reference must be the same function for addEventListener and removeEventListener.
Note Always clear intervals and timeouts in cleanup. Use the updater form (prev => prev + 1) inside intervals to avoid stale closure issues with the state value.
Note Overusing useEffect is one of the most common React mistakes. If you can calculate something from existing props or state, do it during render. If something should happen in response to a user action, put it in the event handler.
avoid useEffectderived stateunnecessary effectwhen not to use effectcompute during render
Note ref.current is null during the first render and gets assigned after the DOM node mounts. Never read or write ref.current during rendering -- only inside effects or event handlers.
dom refuseReffocus inputaccess domdom element reference
Mutable Values (No Re-render)
syntax
const valueRef = useRef(initialValue);
valueRef.current = newValue; // does NOT trigger re-render
Note useRef is like an instance variable for function components. Updating .current does not cause a re-render. Use it for values you need to persist across renders without triggering UI updates: timer IDs, previous values, flags.
mutable refpersist valueno re-renderprevious valueinstance variablestore value across renders
Note In React 19, ref is passed as a regular prop to function components. forwardRef is no longer necessary and is considered deprecated. Existing code using forwardRef still works but should be migrated over time.
forward refref as propforwardRefpass ref to childexpose dom node
Callback Refs
syntax
<element ref={(node) => {
// node is the DOM element or null on unmount
}} />
example
function MeasuredBox() {
const [height, setHeight] = useState(0);
const measureRef = (node) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
};
return (
<div>
<div ref={measureRef} style={{ padding: '20px' }}>
<p>This box has dynamic content.</p>
<p>Its height is measured via a callback ref.</p>
</div>
<p>Measured height: {height}px</p>
</div>
);
}
Note Callback refs fire whenever the ref attaches or detaches. In React 19, callback refs can return a cleanup function that runs when the element is removed, similar to useEffect cleanup. Useful for measuring elements or integrating third-party DOM libraries.
Note useImperativeHandle restricts what the parent can access through the ref. Expose only the methods the parent needs instead of the entire DOM node. Combine with ref-as-prop in React 19 (no forwardRef needed).
useImperativeHandleexpose methodscustom ref handleimperative apiparent control child
Note Store timer IDs in a ref so they survive re-renders without causing them. Always clear the timer on unmount via useEffect cleanup to prevent firing after the component is gone.
store timer iddebouncetimeout refclear timeoutpersist between renders
Note The default value is used only when a component reads the context but there is no matching Provider above it in the tree. Pass a realistic shape as the default so TypeScript inference and fallback behavior work correctly.
Note Wrap the Provider at the highest point where descendant components need the value. If the value is a new object on every render, wrap it with useMemo to prevent unnecessary re-renders of consumers.
provide contextcontext providerwrap with providerpass context value
Consuming Context
syntax
import { useContext } from'react';
const value = useContext(MyContext);
Note useContext always looks for the nearest Provider above the calling component. If none is found, it uses the default value from createContext. The component re-renders whenever the provided value changes.
useContextconsume contextread contextaccess context value
Note Split unrelated data into separate contexts. A single monolithic context forces every consumer to re-render when any part of the value changes, even if they only use a small slice.
// Encapsulate context + hook in a moduleconst Ctx = createContext(null);
exportfunction useMyContext() {
const ctx = useContext(Ctx);
if (!ctx) thrownew Error('Missing Provider');
return ctx;
}
example
import { createContext, useContext, useState } from'react';
const CartContext = createContext(null);
exportfunction CartProvider({ children }) {
const [items, setItems] = useState([]);
function addItem(product) {
setItems(prev => [...prev, { ...product, qty: 1 }]);
}
function removeItem(id) {
setItems(prev => prev.filter(item => item.id !== id));
}
const totalItems = items.reduce((sum, i) => sum + i.qty, 0);
return (
<CartContext.Provider value={{ items, addItem, removeItem, totalItems }}>
{children}
</CartContext.Provider>
);
}
exportfunction useCart() {
const context = useContext(CartContext);
if (!context) {
thrownew Error('useCart must be used within a CartProvider');
}
return context;
}
Note Exporting a custom hook that throws when the Provider is missing catches mistakes early and gives a clear error message instead of undefined values that silently break downstream.
context custom hookcontext with stateprovider patterncart contextglobal state context
Avoiding Unnecessary Re-renders
syntax
// Memoize the value objectconst value = useMemo(() => ({ data, handler }), [data]);
// Split read-only data from dispatch
<DataContext.Provider value={data}>
<DispatchContext.Provider value={dispatch}>
Note Splitting context into a data context and a dispatch context is a powerful pattern. Components that only call actions (dispatch) will not re-render when the data changes, because the dispatch reference stays stable.
Note useMemo caches the result of a computation. React may discard the cache under memory pressure, so never rely on it for correctness -- only for performance. Do not memoize everything by default; profile first.
Note useCallback(fn, deps) is identical to useMemo(() => fn, deps). Its primary use case is passing stable function references to memoized child components (wrapped with React.memo) to prevent their re-renders.
Note Missing a dependency leads to stale closures. Including an unnecessary one causes pointless recalculations. Use the exhaustive-deps lint rule (eslint-plugin-react-hooks) to catch mistakes automatically.
Note React.memo does a shallow comparison of props. It only helps if the component re-renders with the same props frequently. Pair it with useCallback for function props and useMemo for object/array props.
// DO memoize when:// - Computation is genuinely expensive (sorting large arrays, complex math)// - Passing callbacks/objects to React.memo children// - Value is a dependency of another hook// DON'T memoize when:// - The computation is trivial// - The component rarely re-renders// - No downstream consumer cares about reference stability
Note Memoization has a cost: it uses memory to store results and adds comparison overhead. Only apply it where profiling shows a measurable benefit. Premature memoization adds complexity without improving performance.
when to memoizeperformance optimizationshould I useMemomemoization decisionoptimize or not
function DataFetcher({ endpoint, filters }) {
// Without useMemo, a new object is created every render,// causing the effect to re-run endlesslyconst requestConfig = useMemo(
() => ({
url: endpoint,
params: filters,
headers: { 'Accept': 'application/json' },
}),
[endpoint, filters]
);
useEffect(() => {
const controller = new AbortController();
fetch(requestConfig.url, {
...requestConfig,
signal: controller.signal,
}).then(r => r.json()).then(setData);
return () => controller.abort();
}, [requestConfig]);
}
Note When an object or array is a dependency of useEffect, memoize it to prevent infinite effect loops. Every render creates a new reference, and useEffect sees a 'change' even when the values inside are identical.
stable referencereferential equalityprevent infinite loopobject dependencymemoize for effect
Note Prefer useReducer over useState when you have complex state logic with multiple sub-values, or when the next state depends on the previous state in varied ways. The reducer function must be pure -- no side effects.
import { useId } from'react';
function LabeledInput({ label, type = 'text' }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type={type} />
</div>
);
}
// Each instance gets a unique, stable ID
<LabeledInput label="Username" />
<LabeledInput label="Email"type="email" />
Note useId generates IDs that are stable across server and client renders, preventing hydration mismatches. Never use it for keys in a list. It is meant for accessibility attributes (htmlFor, aria-describedby, etc.).
useIdunique idaccessible labelhtmlFor idssr safe id
Note useDeferredValue lets the UI stay responsive by deferring re-rendering of expensive parts. It is similar to debouncing but integrated with React's rendering scheduler. Compare the deferred and current value to show a stale indicator.
useDeferredValuedefer renderresponsive inputdebounce renderingslow list filter
Note startTransition marks state updates as non-urgent, allowing urgent updates (like typing) to interrupt them. The isPending flag lets you show loading indicators. Unlike useDeferredValue, useTransition wraps the state update itself.
Note Designed for subscribing to external data sources (browser APIs, third-party stores) in a way that is compatible with concurrent rendering. The subscribe function must accept a callback and return an unsubscribe function.
useSyncExternalStoreexternal storesubscribebrowser api hookthird party state
Note useDebugValue only affects the React DevTools display for custom hooks. It adds no runtime behavior. Use the format function (second argument) to defer expensive string formatting until the hook is actually inspected in DevTools.
const value = use(resource);
// resource can be a Promise or a Context
example
import { use, Suspense } from'react';
function UserGreeting({ userPromise }) {
// React suspends until the promise resolvesconst user = use(userPromise);
return <h2>Hello, {user.name}!</h2>;
}
function App() {
const userPromise = fetchCurrentUser();
return (
<Suspense fallback={<p>Loading user...</p>}>
<UserGreeting userPromise={userPromise} />
</Suspense>
);
}
Note Unlike other hooks, use() can be called inside if statements and loops. When reading a promise, wrap the component in Suspense. The promise should be created outside the component or cached -- do not create new promises inside render.
use hookread promisesuspense datareact 19 useasync component
use() with Context
syntax
const value = use(SomeContext);
// Can be called conditionally, unlike useContext
Note use(Context) works like useContext but can be placed after early returns or inside conditionals. This is useful when you only need the context value in some code paths.
use context conditionalconditional contextuse vs useContextread context conditionally
Note Actions are async functions passed to the action prop of <form>. They receive FormData as an argument. React handles the pending state and error transitions automatically. Actions work with useActionState and useFormStatus for richer patterns.
form actionactionsasync form submitformDatareact 19 actions
Note useFormStatus must be called from a component rendered inside a <form>. It will not work if called in the same component that renders the form -- it reads from the nearest parent form. Import it from 'react-dom', not 'react'.
useFormStatusform pendingsubmit button loadingform submission statedisable while submitting
Note useOptimistic shows a temporary state while an async action is in progress. When the action completes (or fails), React reverts to the actual state. The optimistic value is automatically rolled back if the action throws.
useOptimisticoptimistic updateinstant ui feedbackoptimistic statetemporary state
Note useActionState combines action handling with state management. The action function receives the previous state as its first argument and FormData as its second. The third return value (isPending) tells you if the action is running.
useActionStateform state actionaction with stateform error handlingserver action state
Note React 19 passes ref as a regular prop to function components. forwardRef is deprecated -- migrate by moving ref into the props destructuring. Existing forwardRef code continues to work but will show deprecation warnings.
ref as propno forwardRefreact 19 refpass refdeprecate forwardRef
function ProductPage({ product }) {
return (
<article>
<title>{product.name} | ShopApp</title>
<meta name="description" content={product.summary} />
<link rel="canonical" href={`/products/${product.slug}`} />
<h1>{product.name}</h1>
<p>{product.description}</p>
<strong>${product.price}</strong>
</article>
);
}
// React hoists these tags into the <head> automatically
Note In React 19, metadata tags rendered inside components are automatically hoisted into the document <head>. This eliminates the need for third-party helmet libraries for basic metadata management. Works with SSR and client rendering.
document titlemeta tagshead metadataseo reactpage titlereact helmet alternative
Note Controlled inputs keep React as the single source of truth. The value prop locks the input to the state. If you set value without onChange, the input becomes read-only.
controlled inputinput stateform inputtext inputinput value
Note Use defaultValue (not value) for uncontrolled inputs. The DOM holds the state. Uncontrolled inputs are simpler for cases where you only need the value at submission time. For dynamic validation or conditional logic, controlled inputs are better.
uncontrolled inputdefaultValueref inputsimple formno state input
Form Submission
syntax
// Traditional
<form onSubmit={handleSubmit}>
// React 19 Actions
<form action={actionFn}>
example
// Traditional approachfunction FeedbackForm() {
const [message, setMessage] = useState('');
const [submitted, setSubmitted] = useState(false);
asyncfunction handleSubmit(e) {
e.preventDefault();
await sendFeedback(message);
setSubmitted(true);
setMessage('');
}
if (submitted) return <p>Thank you for your feedback!</p>;
return (
<form onSubmit={handleSubmit}>
<textarea
value={message}
onChange={e => setMessage(e.target.value)}
required
/>
<button type="submit">Submit</button>
</form>
);
}
Note With onSubmit, always call e.preventDefault() to stop the browser from reloading the page. With React 19 form actions, preventDefault is handled automatically. Both patterns are valid -- actions are newer and reduce boilerplate.
form submithandle submitprevent defaultform onSubmitsend form data
Validation Patterns
syntax
// Validate on change, blur, or submitconst errors = {};
if (!value) errors.field = 'Required';
example
function SignupForm() {
const [fields, setFields] = useState({ name: '', email: '', age: '' });
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
function validate(data) {
const errs = {};
if (!data.name.trim()) errs.name = 'Name is required';
if (!data.email.includes('@')) errs.email = 'Invalid email';
if (Number(data.age) < 18) errs.age = 'Must be at least 18';
return errs;
}
function handleChange(field, value) {
const updated = { ...fields, [field]: value };
setFields(updated);
if (touched[field]) {
setErrors(validate(updated));
}
}
function handleBlur(field) {
setTouched(prev => ({ ...prev, [field]: true }));
setErrors(validate(fields));
}
function handleSubmit(e) {
e.preventDefault();
const errs = validate(fields);
setErrors(errs);
setTouched({ name: true, email: true, age: true });
if (Object.keys(errs).length === 0) {
submitSignup(fields);
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={fields.name}
onChange={e => handleChange('name', e.target.value)}
onBlur={() => handleBlur('name')}
/>
{touched.name && errors.name && <span>{errors.name}</span>}
{/* repeat for other fields */}
<button type="submit">Sign Up</button>
</form>
);
}
Note Validate on blur for a good UX: users see errors after leaving a field, not while typing. On submit, validate everything and mark all fields as touched. For complex forms, consider a form library or useReducer.
form validationvalidate inputerror messagesrequired fieldonBlur validation
Multiple Inputs with One Handler
syntax
function handleChange(e) {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
}
Note Use the name attribute on inputs and a computed property name ([name]) to handle many fields with one function. This scales well and avoids writing separate handlers for each field.
multiple inputssingle handlerdynamic formcomputed property namemany form fields
Note For checkboxes, read e.target.checked (boolean), not e.target.value. For selects, value goes on the <select> element, not on individual <option> elements. Radio buttons in the same group share the same name attribute.
select dropdowncheckboxradio buttonform controlschecked state
Note Pass a function reference, not a function call. Writing onClick={handler()} would call the function during render. Use an arrow function when you need to pass arguments: onClick={() => handler(id)}.
Note React's onChange fires on every keystroke, not just when the field loses focus (which is the native DOM behavior). This makes it ideal for real-time validation and derived values.
Note Always call e.preventDefault() in onSubmit to prevent the browser from performing a full page reload. The submit event fires when pressing Enter inside an input or clicking a submit button.
function handler(event) {
event.target // the DOM element
event.currentTarget // element the handler is attached to
event.preventDefault()
event.stopPropagation()
event.nativeEvent // underlying browser event
}
example
function LinkInterceptor({ children }) {
function handleClick(e) {
// Check if it's an internal linkconst href = e.target.closest('a')?.getAttribute('href');
if (href?.startsWith('/')) {
e.preventDefault();
navigate(href); // client-side navigation
}
}
return <div onClick={handleClick}>{children}</div>;
}
Note React wraps native events in SyntheticEvent objects that work identically across browsers. As of React 17+, events are no longer pooled, so you can access event properties asynchronously without calling e.persist().
function DropZone({ onFileDrop }) {
function handleDragOver(e) {
e.preventDefault(); // Required to allow drop
}
function handleDrop(e) {
e.preventDefault();
e.stopPropagation();
const files = Array.from(e.dataTransfer.files);
onFileDrop(files);
}
return (
<div
onDragOver={handleDragOver}
onDrop={handleDrop}
className="drop-zone"
>
Drop files here
</div>
);
}
Note Use preventDefault for links, form submissions, and drag-and-drop. Use stopPropagation sparingly -- it makes debugging harder. Prefer checking the event target to decide whether to act instead of stopping propagation.
preventDefaultstopPropagationprevent defaultstop bubblingdrag and drop
Capture Phase & Delegation
syntax
// Capture phase (fires before children)
<div onClickCapture={handler}>...</div>
// Event delegation: one handler on parent
<ul onClick={handleItemClick}>
<li data-id="1">A</li>
<li data-id="2">B</li>
</ul>
Note Event delegation (one handler on a parent) reduces the number of event listeners and works well for dynamic lists. Use data-* attributes or closest() to identify which child triggered the event. React internally uses delegation at the root already.
Note Custom hooks must start with 'use' so React can enforce the rules of hooks. They let you extract and share stateful logic between components without changing the component hierarchy.
custom hookextract logicreusable hookshare state logicuseLocalStorage
Compound Components
syntax
function Parent({ children }) { ... }
Parent.Child = function Child(props) { ... };
Note Compound components share implicit state via context, letting you create flexible APIs like <Select> + <Select.Option>. The parent manages state and children consume it without prop drilling.
compound componentscomponent apishared state childrenaccordion patternselect option pattern
Render Props
syntax
function DataProvider({ render }) {
const data = useData();
return render(data);
}
// or via children
<DataProvider>{(data) => <View data={data} />}</DataProvider>
example
function MouseTracker({ children }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
function handleMove(e) {
setPosition({ x: e.clientX, y: e.clientY });
}
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return children(position);
}
// Usage
<MouseTracker>
{({ x, y }) => (
<div>
Cursor is at ({x}, {y})
</div>
)}
</MouseTracker>
Note Render props are largely replaced by custom hooks, which are simpler. Still useful when you need to control what renders based on shared logic without creating a custom hook for every case.
render propsfunction as childrender callbackshared behavior
Error Boundaries
syntax
// Error boundaries must be class components (as of React 19)class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) { logError(error, info); }
render() {
if (this.state.hasError) return <Fallback />;
returnthis.props.children;
}
}
Note Error boundaries catch errors during rendering, in lifecycle methods, and in constructors of the tree below them. They do NOT catch errors in event handlers, async code, or server-side rendering. Use try/catch for those.
Note Portals render children into a different DOM node outside the parent hierarchy, but events still bubble through the React tree (not the DOM tree). Perfect for modals, tooltips, and dropdowns that need to escape overflow:hidden or z-index contexts.
Note React.lazy only works with default exports. For named exports, create an intermediate module that re-exports as default. Suspense can also wrap components that use the use() hook to read promises in React 19.
Note This pattern provides Redux-like state management with built-in React APIs. Splitting state and dispatch into separate contexts prevents components that only dispatch actions from re-rendering when state changes.
Note HOCs were the primary code reuse pattern before hooks. Custom hooks are now preferred for most cases because they are simpler and avoid the wrapper component nesting. HOCs are still useful for cross-cutting concerns like auth guards.
higher order componentHOCwrap componentwithAuthenhance component
Note When two components need the same data, move the state to their closest shared parent and pass it down as props. This is the fundamental React data flow pattern.
lift state upshared statesibling communicationcommon ancestorstate in parent
Prop Drilling Solutions
syntax
// 1. Context// 2. Composition (pass components, not data)// 3. Component composition with children
example
// PROBLEM: drilling "user" through many layers// <App user={user}> -> <Layout user> -> <Header user> -> <Avatar user>// SOLUTION 1: Contextconst UserContext = createContext(null);
// provide at top, consume at Avatar// SOLUTION 2: Composition -- pass the rendered elementfunction App({ user }) {
const avatar = <Avatar user={user} />;
return <Layout header={<Header avatar={avatar} />} />;
}
// Now Layout and Header don't need to know about "user" at allfunction Layout({ header }) {
return (
<div>
<nav>{header}</nav>
<main>...</main>
</div>
);
}
function Header({ avatar }) {
return <div className="header">{avatar}</div>;
}
Note Before reaching for context, try component composition. Passing pre-rendered elements as props skips intermediate layers entirely and keeps the data flow explicit. Context is better for truly global data (theme, auth, locale).
prop drillingpass data deepavoid prop drillingcomponent compositioncontext vs composition
Context for Global State
syntax
// Create -> Provide -> Consumeconst Ctx = createContext(defaultValue);
<Ctx.Provider value={value}>...
const value = useContext(Ctx);
Note Context is ideal for low-frequency updates like theme, locale, and auth. For high-frequency updates (e.g., mouse position), context can cause excessive re-renders -- consider useSyncExternalStore or a dedicated state library.
global statecontext state managementlocale contexti18n reactapp-wide state
When to Use External State
syntax
// Consider external state management when:// - Many components read/write the same state// - State updates are frequent and complex// - You need middleware (logging, persistence, devtools)// - Server state caching is needed (React Query, SWR)
example
// Signs you might need a state library:// 1. Context re-renders are a bottleneck// function App() {// return (// <StoreProvider> {/* every consumer re-renders on any change */}// <BigComponentTree />// </StoreProvider>// );// }// 2. You need selectors (subscribe to a slice)// const count = useStore(state => state.count);// 3. You need server state cache with stale-while-revalidate// const { data, isLoading } = useQuery('users', fetchUsers);// Popular choices:// - Zustand: lightweight, hook-based, selectors// - Jotai: atomic state, bottom-up approach// - React Query / TanStack Query: server state management// - Redux Toolkit: complex client state with devtools
Note Start with useState and useReducer + context. Only add an external library when you hit a concrete pain point: performance issues from context re-renders, complex async logic, or the need for devtools and middleware.
external statestate libraryzustandreduxreact querywhen to use state management
Derived State
syntax
// Compute from existing state during renderconst derivedValue = computeFrom(stateA, stateB);
// Do NOT store derived values in separate state
Note If you can compute a value from existing state or props, derive it inline during render instead of syncing it with useState + useEffect. Derived state in useEffect causes an extra render cycle and introduces potential bugs.
derived statecomputed valuecalculate from stateavoid redundant statesingle source of truth
State Colocation
syntax
// Keep state as close to where it is used as possible// Move state DOWN if only one child needs it// Move state UP only when siblings need to share it
Note Colocating state reduces unnecessary re-renders and makes components easier to understand. Only lift state when it genuinely needs to be shared. Global state for local concerns is an anti-pattern that hurts performance and readability.
state colocationwhere to put statelocal statestate placementstate organization
Note Inline styles use camelCase property names and string/number values. They cannot handle pseudo-classes (:hover), media queries, or keyframe animations. Best for truly dynamic values that change at runtime.
Note CSS Modules scope class names locally by default, preventing naming collisions. The imported styles object maps your authored class names to unique generated ones. Supported out of the box by most React build tools.
Note Use a small classNames helper or the popular 'clsx' package to compose conditional classes cleanly. Avoid complex ternary chains inside className attributes -- extract the logic to keep JSX readable.
// With clsx or classnames libraryimport clsx from'clsx';
<div className={clsx('base', { active: isActive, disabled: isDisabled })} />
example
// Using the clsx libraryimport clsx from'clsx';
function StatusChip({ status }) {
return (
<span
className={clsx('chip', {
'chip--success': status === 'active',
'chip--warning': status === 'pending',
'chip--danger': status === 'error',
'chip--muted': status === 'inactive',
})}
>
{status}
</span>
);
}
// Without a libraryfunction StatusChipManual({ status }) {
const chipClass = [
'chip',
status === 'active' && 'chip--success',
status === 'error' && 'chip--danger',
].filter(Boolean).join(' ');
return <span className={chipClass}>{status}</span>;
}
Note The clsx library (or classnames) accepts strings, objects, and arrays. Object keys are included when their value is truthy. It is lightweight (~228 bytes) and widely used in the React ecosystem.
Note CSS-in-JS co-locates styles with components and supports dynamic props, theming, and automatic vendor prefixing. The tradeoff is runtime overhead and larger bundle size. Consider CSS Modules or Tailwind for better performance in critical paths.
Note Utility-first CSS like Tailwind works well with React's component model -- styles are scoped per component by nature. Use clsx for conditional classes. Extract repeated patterns into components rather than creating custom CSS utility classes.
Note React.memo only does a shallow prop comparison. If you pass objects or functions that are recreated each render, memo has no effect. Pair with useMemo/useCallback for those props.
Note Route-based code splitting gives the biggest bundle size reduction because entire page components and their dependencies are loaded on demand. Place the Suspense boundary around the lazy components, not the entire app.
// 1. Move state down (closer to where it is used)// 2. Lift content up (pass components as children)// 3. Memoize with React.memo + useCallback/useMemo
example
// PROBLEM: typing in input re-renders the expensive listfunction BadPage() {
const [query, setQuery] = useState('');
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<ExpensiveList /> {/* re-renders on every keystroke */}
</div>
);
}
// FIX 1: Move state down into its own componentfunction GoodPage() {
return (
<div>
<SearchInput /> {/* state lives here now */}
<ExpensiveList /> {/* no longer re-renders on keystrokes */}
</div>
);
}
function SearchInput() {
const [query, setQuery] = useState('');
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
// FIX 2: Lift content up via childrenfunction InputWrapper({ children }) {
const [query, setQuery] = useState('');
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
{children} {/* children don't re-render */}
</div>
);
}
function Page() {
return (
<InputWrapper>
<ExpensiveList />
</InputWrapper>
);
}
Note Before reaching for React.memo, try restructuring your component tree. Moving state down and lifting content up are free optimizations that often eliminate the problem entirely.
avoid re-rendersreduce rendersmove state downlift content uprender optimization
Key Prop for Efficient Updates
syntax
// Stable keys help React identify which items changed
<Item key={item.id} />
// Reset component state by changing the key
<Form key={selectedUserId} />
example
// Using key to reset component statefunction UserEditor({ userId }) {
// When userId changes, React destroys the old Form// and mounts a fresh one with clean statereturn <ProfileForm key={userId} userId={userId} />;
}
function ProfileForm({ userId }) {
const [name, setName] = useState('');
const [bio, setBio] = useState('');
useEffect(() => {
fetchUser(userId).then(user => {
setName(user.name);
setBio(user.bio);
});
}, [userId]);
return (
<form>
<input value={name} onChange={e => setName(e.target.value)} />
<textarea value={bio} onChange={e => setBio(e.target.value)} />
</form>
);
}
Note Changing a component's key forces React to unmount and remount it, resetting all internal state. This is a clean way to 'reset' a form or component when switching between items without manually clearing each state variable.
key propreset componentkey optimizationforce remountchange key reset state
import { Profiler } from'react';
function onRenderCallback(
id,
phase, // 'mount' or 'update'
actualDuration, // time spent rendering
baseDuration, // estimated time without memoization
startTime,
commitTime
) {
console.log(`${id} [${phase}]: ${actualDuration.toFixed(1)}ms`);
}
function App() {
return (
<Profiler id="ProductList" onRender={onRenderCallback}>
<ProductList />
</Profiler>
);
}
Note Use the Profiler component or React DevTools Profiler tab to identify slow renders. Focus on components with high actualDuration. The baseDuration shows the worst-case cost without any memoization.
Note For lists with hundreds or thousands of items, virtualization dramatically improves performance by only rendering visible rows. react-window is lightweight; react-virtuoso supports dynamic heights. Measure first -- small lists do not need this.
// PROBLEM: handler captures old state value// FIX: use updater function or ref
example
function BrokenCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
// BUG: count is always 0 (captured from initial render)
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []); // empty deps = closure captures initial countreturn <p>{count}</p>; // stuck at 1
}
function FixedCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
// FIX: updater function always has the latest value
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <p>{count}</p>; // increments correctly
}
Note Stale closures happen when a callback captures a state value that never updates. Common in setInterval, setTimeout, and event listeners set up in useEffect. Use the updater form of setState or store values in a ref.
stale closureold state valueclosure bugsetInterval stalestate not updating
Direct State Mutation
syntax
// BAD: mutating state directly
state.items.push(newItem);
setState(state);
// GOOD: create a new reference
setState({ ...state, items: [...state.items, newItem] });
example
function BrokenTags() {
const [tags, setTags] = useState(['react', 'hooks']);
function addTag(tag) {
// BUG: push mutates the existing array
tags.push(tag);
setTags(tags); // same reference, React sees no change
}
return (
<div>
{tags.map(t => <span key={t}>{t}</span>)}
<button onClick={() => addTag('new')}>Add</button>
</div>
);
}
function FixedTags() {
const [tags, setTags] = useState(['react', 'hooks']);
function addTag(tag) {
// CORRECT: spread creates a new array
setTags(prev => [...prev, tag]);
}
return (
<div>
{tags.map(t => <span key={t}>{t}</span>)}
<button onClick={() => addTag('new')}>Add</button>
</div>
);
}
Note React uses reference equality (Object.is) to decide whether to re-render. Mutating an object or array in place keeps the same reference, so React skips the update. Always produce a new object/array via spread, map, filter, or slice.
state mutationmutate state directlypush state arraystate not updatingimmutable update
Missing or Bad Keys
syntax
// BAD: index as key with reorderable list
{items.map((item, i) => <Item key={i} />)}
// GOOD: stable unique identifier
{items.map(item => <Item key={item.id} />)}
example
// BUG: using index as key with a removable listfunction BrokenList() {
const [items, setItems] = useState([
{ id: 'a', text: 'First' },
{ id: 'b', text: 'Second' },
{ id: 'c', text: 'Third' },
]);
function removeFirst() {
setItems(prev => prev.slice(1));
}
return (
<div>
{/* Using index: after removing first item,
React thinks items shifted, inputs lose state */}
{items.map((item, index) => (
<div key={index}>
<input defaultValue={item.text} />
</div>
))}
<button onClick={removeFirst}>Remove First</button>
</div>
);
}
// FIX: use item.id as key// {items.map(item => <div key={item.id}>...)}
Note Index keys cause broken behavior when items are added, removed, or reordered. React associates state (including uncontrolled input values) with the key. When keys shift, state gets attached to the wrong item.
key propmissing keyindex as keylist key bugwrong keykey warning
useEffect Dependency Issues
syntax
// MISSING DEPS: effect uses stale values
useEffect(() => { fn(a, b); }, []); // a, b missing// OBJECT DEPS: infinite loop
useEffect(() => { ... }, [{ x: 1 }]); // new ref each render
example
// BUG: missing dependencyfunction Greeting({ name }) {
const [message, setMessage] = useState('');
useEffect(() => {
// name is used but not in deps -- stale value
setMessage(`Hello, ${name}!`);
}, []); // should be [name]return <p>{message}</p>;
}
// BUG: object dependency causes infinite loopfunction DataLoader({ filters }) {
useEffect(() => {
fetchData(filters);
}, [filters]); // if parent recreates filters object each render, infinite loop!// FIX: destructure primitive valuesconst { category, minPrice } = filters;
useEffect(() => {
fetchData({ category, minPrice });
}, [category, minPrice]);
}
Note Enable the eslint-plugin-react-hooks exhaustive-deps rule to catch missing dependencies. For object dependencies, extract primitive values or memoize the object in the parent. Never suppress the lint rule without understanding why.
// CAUSE 1: setState during renderfunction Bad() {
const [x, setX] = useState(0);
setX(1); // triggers re-render during render!
}
// CAUSE 2: object/array in useEffect deps
useEffect(() => { ... }, [{ a: 1 }]); // new ref every render
example
// BUG: calling setState unconditionally during renderfunction BrokenComponent({ data }) {
const [processed, setProcessed] = useState(null);
// This runs on every render, which triggers a new render...
setProcessed(transform(data));
return <Display data={processed} />;
}
// FIX: derive the value without statefunction FixedComponent({ data }) {
const processed = transform(data); // compute inlinereturn <Display data={processed} />;
}
// BUG: onClick={handler()} calls immediatelyfunction BrokenButton() {
const [count, setCount] = useState(0);
// handler() executes during render, calling setCount, loopingreturn <button onClick={setCount(count + 1)}>Click</button>;
}
// FIX: pass function referencefunction FixedButton() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Click</button>;
}
Note Infinite re-render loops crash your app. The most common causes: (1) calling setState directly in the render body, (2) onClick={fn()} instead of onClick={fn}, (3) useEffect with an object/array literal in the dependency array that creates a new reference every render.
infinite re-rendertoo many rendersrender loopmaximum update depthsetState during render
Overusing useEffect
syntax
// ANTI-PATTERN: effect to sync derived state
useEffect(() => { setB(computeFrom(a)); }, [a]);
// BETTER: compute inlineconst b = computeFrom(a);
Note Each unnecessary useEffect + setState causes an extra render cycle. If a value can be derived from props or state, compute it during render. Reserve useEffect for genuine side effects: API calls, subscriptions, DOM manipulation, and third-party library integration.
overuse useEffectuseEffect anti-patterneffect chainderived state effectunnecessary effect
Setting State After Unmount
syntax
// PROBLEM: async callback sets state on unmounted component
useEffect(() => {
fetchData().then(data => setState(data)); // may be unmounted!
}, []);
// FIX: track mounted status
useEffect(() => {
let active = true;
fetchData().then(data => { if (active) setState(data); });
return () => { active = false; };
}, []);
example
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
asyncfunction loadUser() {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
// Only update state if this effect is still activeif (!cancelled) {
setUser(data);
}
}
loadUser();
return () => {
cancelled = true;
};
}, [userId]);
if (!user) return <p>Loading...</p>;
return <h1>{user.name}</h1>;
}
Note While React 18+ no longer warns about this, it is still a logic bug. Without the cancelled flag, a slow request for user A could resolve after switching to user B, overwriting user B's data with user A's data. Always use a cleanup flag or AbortController.
unmount state updatecancelled requestrace conditionabort fetchcleanup async
State Batching Behavior
syntax
// React 18+ batches ALL state updates automaticallyfunction handleClick() {
setA(1); // does not trigger render
setB(2); // does not trigger render
setC(3); // single render with all three updates
}
example
function StatusPanel() {
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
asyncfunction loadData() {
setLoading(true);
setError(null);
// React batches these two -- one rendertry {
const result = await fetchDashboardData();
setData(result);
setLoading(false);
// React batches these two -- one render
} catch (err) {
setError(err.message);
setLoading(false);
// React batches these two -- one render
}
}
return (
<div>
{loading && <p>Loading...</p>}
{error && <p>Error: {error}</p>}
{data && <Dashboard data={data} />}
</div>
);
}
Note Since React 18, automatic batching applies to all updates, including those in promises, setTimeout, and native event handlers. You rarely need to worry about batching anymore. If you ever need to force a synchronous render, use flushSync (imported from react-dom) as a last resort.
state batchingmultiple setStateautomatic batchingflushSyncrender batching