
React Performance
- 25 installs
- 1 repo stars
- Updated August 4, 2026
- robsonrung/rar-skills
Advises on and reviews React code for unnecessary re-renders, memoization traps, Context performance, stale closures, and async data-fetching safety.
About
A React lens from Advanced React that steers new code toward composition over memoization and audits existing components for re-render, closure, and async issues. A developer uses it while writing or reviewing a React component, hook, or context.
- Audits unnecessary re-renders, memoization traps and Context performance
- Covers stale closures, fetch race conditions, useLayoutEffect flicker and error boundaries
React Performance by the numbers
- 25 all-time installs (skills.sh)
- +5 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,503 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/robsonrung/rar-skills --skill react-performanceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 25 |
|---|---|
| repo stars | ★ 1 |
| Last updated | August 4, 2026 |
| Repository | robsonrung/rar-skills ↗ |
What it does
Advises on and reviews React code for unnecessary re-renders, memoization traps, Context performance, stale closures, and async data-fetching safety.
Files
React Performance & Patterns Lens
Advise and review React code using the rules from Advanced React by Nadia Makarevich. It asks one set of questions: will this re-render more than it needs to, is memoization actually doing anything, is this closure stale, and is the async path safe? Not a bug hunt for logic errors — use code-review for that.
Two modes, pick by context:
- Advisor (while writing new React code): steer the decision before the code is
written — prefer composition over memo, place state correctly, structure the fetch.
- Reviewer (on existing/changed code): audit against the checks below, report findings
grouped by lens. Cite file:line, name the rule, propose the concrete fix. Skip lenses that don't apply rather than padding. If a component is clean, say so plainly.
Repo context (read before applying)
- Frontend is React 17 (see
frontend/CLAUDE.mdat the app repo root): no
automatic batching outside React event handlers, forwardRef is still required to pass a ref prop, and the React 19 "ref as a regular prop" change does not apply here. The book (2023) matches React 17/18 closely — prefer its advice over half-remembered React 19 behavior.
- MUI component trees are deep and re-render-sensitive; unnecessary parent re-renders
cascade through styled components.
- Redux Toolkit is already the external store with memoized selectors. For
cross-tree shared state, reach for RTK selectors over a hand-rolled Context; use Context for low-frequency, localized config (theme, current entity), not hot state.
The golden rule (applies to every lens below)
Composition first, memoization last. React.memo is the last resort aftercomposition techniques have failed — because memoizing all props correctly is harder
than it looks, and one missed non-primitive prop silently defeats it.
---
Lens 1 — Unnecessary re-renders (ch 1–4)
A state update re-renders the owning component and all of its children, regardless of props. Props only matter once React.memo is involved. So the cheapest fix is structural, not memoization.
Check, in order:
1. Move state down. Is fast-changing state (an input value, a hover flag, an open/closed toggle) held high in the tree, forcing a big subtree to re-render? Extract the state + the small piece that uses it into a child component. This is the single highest-value fix in the book. 2. Children / elements as props. When a component owns state but renders a heavy subtree that doesn't depend on that state, pass the subtree as children (or as an element prop). Elements passed in as props don't re-render when the parent's own state changes — they were created by the parent's parent. Exact pattern: cheatsheet ch 2. 3. Render props only when the children genuinely need the parent's state/DOM data (e.g. logic attached to a DOM element). Hooks replaced ~99% of the old "share stateful logic" use case — don't reach for render props just to share logic.
Red flags: a top-level page component holding an input's value; a useState in a provider or layout that wraps half the app; "I'll wrap it in memo" as the first idea.
Lens 2 — Memoization that does nothing (ch 5)
React compares objects/arrays/functions by reference, in memo props and in hook deps. useMemo memoizes a result; useCallback memoizes the function itself.
Flag memoization that buys nothing — useMemo/useCallback is only justified when one of these holds:
- the value is a dependency of another hook (
useEffect/useMemo/useCallback), or - the value is a prop passed to a component wrapped in
React.memo, or - it's passed down to a component that then hits one of the above.
And flag React.memo that is silently defeated:
- a non-primitive prop (object/array/function/`children`) passed to the memo'd
component is recreated each render → memo is useless. All props must be primitive or stable. children is a prop too — memoizing it is easy to forget.
- the value came from another non-memoized prop or hook result → memoization chain is
broken.
Rule of thumb: if you're memoizing a prop, prove the consumer is React.memo'd or uses it as a dep. Otherwise delete the memo.
Memo'd list items, or state mysteriously resetting/persisting across renders → keys & reconciliation, cheatsheet ch 6.
Lens 3 — Context performance (ch 8)
Every consumer of a Context re-renders when the provider value changes — and standard memoization can't stop it.
- Flag a provider whose
value={{...}}/value={[a, b]}is an **unmemoized inline
object/array** — every parent render forces every consumer to re-render. Wrap it in useMemo (and callbacks in useCallback).
- For multiple unrelated values, split into multiple providers so a change to one
doesn't re-render consumers of the other. useState → useReducer helps keep the data and the API in separate stable contexts.
- No real selectors exist for Context; you can fake them with
React.memo+ HOCs (see
cheatsheet ch 7), but if you find yourself doing that, use RTK (this repo already has it) instead.
Lens 4 — Refs, closures & stale state (ch 9–11)
- A ref is a mutable box preserved across renders; mutating
ref.currentdoes not
re-render and is synchronous. Use it for values that must persist but shouldn't trigger renders (timers, latest-callback, previous value, DOM nodes). Don't use it for anything that should appear in the UI.
- `forwardRef` is required (React 17) to pass a
refprop to a function component;
expose a controlled imperative API with useImperativeHandle rather than leaking the DOM node.
- Stale closure = the #1 bug here. A function created in render "freezes" the state and
props it closed over. If it's memoized (useCallback/useMemo) with a missing dep, or stored in a ref once, it keeps reading old values.
- Detect: a
useCallback/useEffect/useMemoreading state/props but missing them from
deps; a callback stored in ref.current that's never refreshed.
- Escape: refresh the closed function in
ref.currentinside auseEffecton every
render, then call ref.current() — it always sees the latest data. This is also how you keep a React.memo child stable while still calling fresh logic.
Lens 5 — Debounce / throttle (ch 11)
debounce/throttle only work if the same instance lives across the component's life.
- Flag
debounce(...)/throttle(...)/setTimeoutid created inline in render or
in a non-memoized handler — the timer is recreated every render and never fires correctly.
- Fix: memoize the debounced function once (
useMemo/ref), and access the latest state via
the ref-refresh trap from Lens 4 (a naive memo freezes state at creation time).
Lens 6 — Flickering UI / useLayoutEffect (ch 12)
Measure-then-mutate DOM work (size/position) in useEffect lets the browser paint the "before" frame first → visible glitch. Use `useLayoutEffect` — it runs synchronously before paint. SSR caveat and exact pattern: cheatsheet ch 12.
Lens 7 — Portals & stacking context (ch 13)
Modals/tooltips/dropdowns clipped by an ancestor's overflow or trapped in a Stacking Context (nothing escapes one, not even position: fixed) → render via a Portal. MUI's Modal/Popper already portal — flag hand-rolled overlays that don't. CSS rules and event-bubbling behavior: cheatsheet ch 13.
Lens 8 — Data fetching: waterfalls & race conditions (ch 14–15)
- Waterfall: sequential/conditional
awaits that could be parallel. Flag dependent
useEffect fetches and await a; await b where a and b are independent → use Promise.all, parallel promises, or a data-provider Context. Mind browser parallel request limits; critical resources can be prefetched before React mounts.
- Race condition:
setStateafter anawait/.thenin an effect keyed on a changing
value (e.g. url, an id). A slow earlier request can resolve after a newer one and overwrite fresh data. Fixes (prefer the last two): compare the resolved id before setState; cleanup-flag in useEffect; `AbortController`. Exact patterns: cheatsheet ch 15. (RTK Query handles this for you — prefer it over hand-rolled fetch in effects when the data is server state.)
Lens 9 — Error handling (ch 16)
After React 16, an uncaught render error unmounts the whole app — a few ErrorBoundarys in strategic places are non-negotiable. Boundaries miss callbacks/setTimeout/promises; try/catch misses render/useEffect/nested components. Bridge by re-throwing async errors into the render lifecycle, or use the react-error-boundary library. Exact hook and coverage matrix: cheatsheet ch 16.
---
How to apply
1. Identify which lenses the change touches (a fetch hook → 8; a provider → 3; a memo'd component → 1, 2, 4). 2. In advisor mode, recommend the structural option first (move state down / composition) and only escalate to memoization when the checks in Lens 2 are met. 3. In reviewer mode, report per lens: file:line → which rule → concrete fix. 4. Don't invent re-renders that don't matter — a component that renders cheaply and rarely needs no optimization. Optimize where state is hot and the subtree is heavy.
See references/cheatsheet.md for the per-chapter decision tables and code snippets.
Advanced React — per-chapter cheat sheet
Source: Advanced React by Nadia Makarevich (2023). Page-level decision tables and the canonical code snippets behind each lens in SKILL.md. Consult a section when a lens fires and you need the exact pattern.
---
Ch 1 — Re-renders
- Re-render = React calling a component's function again. **State update is the only initial
source** of re-renders (plus parent re-render, context change).
- A re-render flows down, never up: a state change re-renders the owner and every
nested component, regardless of props. React never re-renders parents because a child changed.
- Without memoization, props don't matter — children re-render even with no props.
- A state update inside a hook re-renders the component using that hook, even if the state
value is never read; and through a chain of hooks, any update re-renders the consumer of the first hook.
"Moving state down" — the primary fix:
// ❌ value lives in App → typing re-renders <VerySlowComponent/>
const App = () => {
const [value, setValue] = useState('');
return (<><input value={value} onChange={e => setValue(e.target.value)} /><VerySlowComponent /></>);
};
// ✅ isolate the state + its consumer; App (and the slow tree) no longer re-render on type
const SearchInput = () => {
const [value, setValue] = useState('');
return <input value={value} onChange={e => setValue(e.target.value)} />;
};
const App = () => (<><SearchInput /><VerySlowComponent /></>);Ch 2 — Elements, children as props
- Component = a function
(props) => Elements. Element = the object<B />
produced; type is a string (DOM) or a component reference.
- A component re-renders when its element object changes (by
Object.is). - Elements passed as props (including
children) are created by the parent's parent,
so they don't re-render when the receiving component updates its own state.
childrenis justprops.children;<Parent><Child/></Parent>≡<Parent children={<Child/>}/>.
// state in ScrollDetector changes constantly; {children} passed in does NOT re-render
const ScrollDetector = ({ children }) => {
const [scroll, setScroll] = useState(0);
return <div onScroll={e => setScroll(e.target.scrollTop)}>{children}</div>;
};
<ScrollDetector><SlowComponent /></ScrollDetector>;Ch 3 — Configuration via elements as props
- Push configuration of a rendered child up to the consumer by accepting the whole element:
<Button icon={<Error color="red" size="large" />} />.
- An element stored in a variable but passed to a conditionally-rendered component is only
rendered when that component actually renders:
const footer = <Footer />; // not rendered yet
return isDialogOpen ? <ModalDialog footer={footer} /> : null;- To inject/override default props onto an element-prop, use
React.cloneElement.
Ch 4 — Render props
- Convert an element-prop to a render prop when the parent must control its props or
feed it state:
const Button = ({ renderIcon }) => {
const [state, setState] = useState();
return <button>Submit {renderIcon({ size: 'large' }, state)}</button>;
};
<Button renderIcon={(props, state) => <Icon {...props} active={state} />} />;childrencan be a render prop:const Parent = ({ children }) => children(data);.- Hooks replaced render-props-for-logic in ~99% of cases. Keep render props mainly for
logic tied to a DOM element (size/position trackers, etc.).
Ch 5 — Memoization (memo / useMemo / useCallback)
- Reference comparison only: objects/arrays/functions differ every render unless memoized.
An inline fn passed to useMemo/useCallback is recreated each render (that's expected — the output is what's stabilized). useCallback(fn) ≈ useMemo(() => fn).
- Memoizing a prop helps only if the consumer is
React.memoand uses it as a dep, or
it's passed further down into such a situation. Otherwise the memo is dead weight.
React.memoskips a re-render triggered by the parent only when all props are
unchanged by reference. Triggers from own state/context still re-render.
- Memoizing all props is harder than it looks: avoid passing non-primitive values sourced
from other props/hooks; `children` is non-primitive too and must be memoized.
- Order of preference: composition (ch 1–4) → then
React.memoas a last resort.
Ch 6 — Diffing & reconciliation
- React diffs by position in the returned array + type: same type+position → update
in place; type change at a position → unmount old, mount new (state lost).
- Conditional
cond ? <A/> : <B/>occupies one array slot (even anullbranch). - Dynamic arrays need `key` — stable identity across reorder/add/remove; critical when
items are React.memo'd.
keyis a general tool, not array-only:- same type+position + changing
key→ force a remount ("state reset", e.g. on route
change).
- use
keyto make React treat two same-type elements as the same/different deliberately.
Ch 7 — Higher-order components
- HOC =
(Component) => (props) => <Component {...props} injected="x" />. Can inject props
and logic (hooks allowed inside the returned component).
- Modern use: cross-cutting concerns (logging, feature flags, fake context selectors). For
shared stateful logic, prefer hooks.
Ch 8 — Context & performance
- Every consumer re-renders when provider
valuechanges; **memoization in consumers can't
stop it**.
- Always memoize the provider value (and any callbacks in it):
const value = useMemo(() => ({ user, setUser }), [user]);
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;- Split providers so unrelated values don't co-trigger;
useReducerseparates stable
dispatch from changing state, enabling two contexts (state vs API).
- No native selectors; emulate with
React.memo+ HOC, but for anything non-trivial use
RTK (already in this repo) with memoized selectors.
Ch 9 — Refs
- Ref = mutable
{ current }preserved across renders; updates are synchronous and do
not re-render. Good for: timers, latest-value tracking, previous value, DOM nodes.
<div ref={r} />→r.currentis the DOM node after render. Refs can be passed as normal
props.
- To pass the real
refprop to a function component (React 17):forwardRef.
const InputField = forwardRef((props, ref) => <input ref={ref} />);- Expose a controlled imperative API with
useImperativeHandleinstead of leaking the node:
useImperativeHandle(apiRef, () => ({ focus: () => {}, shake: () => {} }));Ch 10 — Closures
- A closure forms whenever a function is created inside another; React components are
functions, so useCallback/useMemo/useRef callbacks all close over render-time data.
- When called later, that data is a frozen snapshot. To refresh it, re-create the
function — that's what hook deps do. Missing dep → stale closure.
- Stale-closure escape via ref:
const ref = useRef();
useEffect(() => { ref.current = () => console.log(value); }); // refresh every render
const onClick = useCallback(() => ref.current(), []); // stable, yet sees latestCh 11 — Debounce / throttle with refs
- Debounced/throttled fns must be created once (component mount), else the internal
timer resets every render and never fires.
- Memoize the debounced fn, but a naive memo freezes state — combine with the ref-refresh
trap so it reads the latest value:
const onChange = useMemo(() => debounce(() => ref.current(), 500), []);Ch 12 — useLayoutEffect (flicker)
useEffectruns async → browser may paint the pre-mutation frame → visible glitch when
you measure-then-move/resize/hide DOM.
useLayoutEffectruns synchronously before paint → one unbreakable task, no glitch.- Doesn't run in SSR (React skips it); opt out of SSR for that feature if needed. (N/A for
this Amplify SPA.)
Ch 13 — Portals & stacking context
position: absolute→ relative to positioned ancestor; clipped byoverflow: hidden.position: fixed→ relative to viewport; escapesoverflow: hiddenbut not a
Stacking Context.
- Nothing escapes a Stacking Context (formed by
position+z-index,transform,
translate, opacity, filters, …).
- Portal renders DOM outside the subtree (modals, tooltips). React events bubble along
the React tree; layout follows the portal target. MUI Modal/Popper/Tooltip already portal — flag hand-rolled overlays that don't.
Ch 14 — Data fetching & waterfalls
- Two categories: initial vs on-demand. Plain
fetchworks but you reimplement
caching/dedup/race handling manually.
- Waterfall = requests that run in sequence/conditionally when they could be parallel.
Avoid with Promise.all, parallel-started promises, or a data-provider Context.
- Mind browser parallel-connection limits; prefetch critical resources before React mounts
(within those limits).
Ch 15 — Race conditions
- Risk:
setStateafter anawait/.thenin an effect keyed on a changing value. An older
request can resolve last and clobber newer data.
useEffect(() => { fetch(url).then(r => r.json()).then(setData); }, [url]); // ⚠- Fixes (best last):
1. remount component (key change) to discard old data, 2. compare resolved id vs current before setState, 3. cleanup flag in useEffect drops stale results, 4. `AbortController` cancels previous requests.
useEffect(() => {
const ac = new AbortController();
fetch(url, { signal: ac.signal }).then(r => r.json()).then(setData).catch(() => {});
return () => ac.abort();
}, [url]);- Prefer RTK Query for server state — it handles dedup, caching, and stale-response
discarding for you.
Ch 16 — Error handling
- Post-React-16: an uncaught render error unmounts the whole app. Place several
ErrorBoundarys at strategic points (route, major panel).
ErrorBoundarycatches errors from anywhere down the tree, but not in callbacks,
setTimeout, or promises.
try/catchcatches async/callback errors, but not errors from nested components,
render, or useEffect.
- Merge them: catch async errors with
try/catch, then re-throw into render so the
boundary catches them:
const useAsyncError = () => {
const [, setError] = useState();
return useCallback(e => setError(() => { throw e; }), []);
};Or use the react-error-boundary library.