---
name: react-render-performance
description: >
Minimize unnecessary React re-renders when consuming external state (XState, @xstate/store,
Zustand, Redux, Nanostores, context). Prefer selector-based subscriptions over useState(wholeObject).
Use when dealing with external state in React, optimizing re-renders, choosing state patterns,
or integrating with these libraries.
---
# React Render Performance
Patterns for minimizing unnecessary React re-renders when consuming external
state. **Prefer selector-based subscriptions over `useState(wholeObject)`** —
subscribe only to the slice each component needs.
## Core idea
Storing a full state object in React state (e.g. `useState(snapshot)` and
subscribing to every change) forces re-renders on **any** update. A component
that only needs `phase` will still re-render when `quiz.selectedWrong` changes
if both live in the same object.
**Avoid:** subscribe → `setState(fullObject)` → read a field in render.
**Prefer:** subscribe to a **selector** or **slice** so the component re-renders
only when that value changes. Every library below supports this; use it.
**Tree position matters.** The higher a component is in the tree, the more
expensive a re-render becomes, because React re-renders it and all descendants.
Subscribing to the whole store in `App.tsx` is especially bad — every store
change re-renders the entire app. Push subscriptions down to the leaf or
route-level components that actually need the data, or use selectors so
high-level components only re-render when their slice changes.
---
## Library patterns
### XState (actors) — `@xstate/react`
Use `useSelector(actor, selector)` so the component re-renders only when the
selected value changes.
```tsx
// GOOD: stable selector — re-renders only when phase changes
import { useSelector } from "@xstate/react";
import { selectPhase } from "./selectors";
function PhaseIndicator({ actor }) {
const phase = useSelector(actor, selectPhase);
return {phase};
}
```
```tsx
// BAD: full snapshot in React state — re-renders on every actor change
const [snapshot, setSnapshot] = useState(null);
useEffect(() => {
const sub = actor.subscribe((snap) => setSnapshot(snap));
return () => sub.unsubscribe();
}, [actor]);
const phase = snapshot?.value?.sessionFlow; // unnecessary re-renders
```
**Actor + ref for callbacks:** Keep the actor in `useState` (so `useSelector`
re-subscribes if the actor is replaced) and in a `useRef` for synchronous
access in event handlers:
```tsx
const [actor, setActor] = useState(() => {
const a = createActor(machine);
a.start();
return a;
});
const actorRef = useRef(actor);
actorRef.current = actor;
function send(event) {
actorRef.current.send(event);
}
```
---
### @xstate/store — `@xstate/store-react`
Use `useSelector(store, selector)` to subscribe to a slice of store context.
Re-renders only when the selected value changes (strict equality by default;
optional custom `compare`).
```tsx
// GOOD: select one field — re-renders only when count changes
import { createStore, useSelector } from "@xstate/store-react";
const store = createStore({
context: { count: 0, name: "" },
on: { inc: (ctx) => ({ ...ctx, count: ctx.count + 1 }) },
});
function CountDisplay() {
const count = useSelector(store, (state) => state.context.count);
return {count};
}
```
```tsx
// BAD: selecting whole context — re-renders on any context change
const context = useSelector(store, (state) => state.context);
return {context.count};
```
Custom comparison when the selector returns an object:
```tsx
const user = useSelector(
store,
(state) => state.context.user,
(prev, next) => prev.id === next.id
);
```
---
### Zustand
Use the store with a **selector** as the first argument. The component
re-renders only when the selected value changes (referential equality).
```tsx
// GOOD: selector — re-renders only when count changes
const count = useStore((state) => state.count);
// GOOD: primitive or stable ref — minimal re-renders
const phase = useStore((state) => state.session.phase);
```
```tsx
// BAD: no selector — re-renders on every store change
const state = useStore();
return {state.count};
```
```tsx
// BAD: selecting a new object every time — re-renders every time
const { count, name } = useStore((state) => ({ count: state.count, name: state.name }));
// Use two selectors or useShallow instead
```
Use a **module-level selector** so the function reference is stable (see
Selector rules below). For multiple fields, use `useShallow` or pick
primitives:
```tsx
import { useShallow } from "zustand/react/shallow";
const { count, name } = useStore(useShallow((state) => ({ count: state.count, name: state.name })));
```
---
### Redux — `react-redux`
Use `useSelector(selector)` and select the smallest slice needed. Redux uses
referential equality; selecting a new object every time forces re-renders.
```tsx
// GOOD: select a primitive or stable reference
const phase = useSelector((state) => state.session.phase);
const count = useSelector((state) => state.counter);
```
```tsx
// BAD: selecting whole slice — new object ref when any part of session updates
const session = useSelector((state) => state.session);
return {session.phase};
```
For object slices use `shallowEqual` or a memoized selector:
```tsx
import { shallowEqual, useSelector } from "react-redux";
const { phase, step } = useSelector(
(state) => ({ phase: state.session.phase, step: state.session.step }),
shallowEqual
);
```
---
### Nanostores — `@nanostores/react`
Nanostores doesn’t take a selector in the hook; **shape your stores so each
consumer subscribes to a small store**. Use **computed** stores to derive
slices, or split state into multiple atoms.
```tsx
// GOOD: one atom per logical slice, or computed for a derived slice
import { atom, computed } from "nanostores";
import { useStore } from "@nanostores/react";
const $session = atom({ phase: "idle", step: 0 });
const $phase = computed($session, (s) => s.phase);
function PhaseIndicator() {
const phase = useStore($phase); // re-renders only when phase changes
return {phase};
}
```
```tsx
// BAD: one big store, useStore on the whole thing — re-renders on any change
const $app = atom({ session: {...}, quiz: {...}, ui: {...} });
function PhaseIndicator() {
const app = useStore($app);
return {app.session.phase};
}
```
Use **map** or **atoms** for granular updates and **computed** for derived
values; then each component `useStore`s only the store it needs.
---
### React context
Context re-renders all consumers when the value reference changes. Prefer
**splitting by update frequency** or **exposing a subscribable store** and
selecting in the consumer.
```tsx
// GOOD: split by update frequency
{children}
```
```tsx
// GOOD: store in context, select in consumer (e.g. Zustand store, XState actor)
function useSessionPhase() {
const store = useContext(StoreContext);
return useSelector(store, (s) => s.phase);
}
```
```tsx
// BAD: one context with everything — any change re-renders all consumers
```
---
### useSyncExternalStore (custom stores)
For stores that aren’t one of the above, use React’s `useSyncExternalStore` and
subscribe to a **slice** in `getSnapshot` so the component only re-renders
when that slice changes.
```tsx
// GOOD: getSnapshot returns only the slice this component needs
const phase = useSyncExternalStore(
store.subscribe,
() => store.getSnapshot().session.phase,
() => store.getSnapshot().session.phase
);
```
```tsx
// BAD: getSnapshot returns full state — re-renders on every store change
const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
return {state.session.phase};
```
---
## Selector rules
1. **Keep selectors at module level** — not inline in the component. Inline
arrow functions create new references each render and can defeat equality
checks.
```tsx
// GOOD
const selectPhase = (snap) => snap.value?.sessionFlow;
function MyComponent({ actor }) {
const phase = useSelector(actor, selectPhase);
}
// BAD — new function ref every render
function MyComponent({ actor }) {
const phase = useSelector(actor, (snap) => snap.value?.sessionFlow);
}
```
2. **Return primitives or stable references.** If the selector returns a new
object/array every time, the component will re-render on every update. Prefer
primitives or use a custom comparison when you must return an object.
3. **Don’t put expensive derivation in selectors.** Heavy work belongs in
`useMemo` in the component, not in the selector (selectors run often).
---
## Anti-patterns
| Anti-pattern | Why it's bad | Fix |
|--------------|--------------|-----|
| `setState(fullSnapshot)` in subscribe | Every store/actor change re-renders | Use selector / slice (useSelector, selector arg, computed store) |
| No selector / whole store in hook | Same as above | Pass selector to useStore/useSelector; or use computed/small stores |
| Inline selector function | New reference each render | Module-level selector |
| Selector returns new object every time | Always re-renders | Return primitive or use shallowEqual/custom compare |
| Mega-context with everything | Any update re-renders all consumers | Split context or put a store in context and select in consumer |
---
## When to use selectors
**Use a selector / slice when:**
- The component needs 1–2 fields from a larger state
- Different fields update at different rates (e.g. phase rarely, quiz state often)
- Several components each need different parts of the same store
- The component is **high in the tree** (e.g. `App.tsx`, layout, root route) — re-renders there cascade down the whole tree, so avoid subscribing to the whole store at that level
**A single subscription is OK when:**
- The component needs most or all of the state
- Updates are rare (e.g. user profile)
- There’s only one consumer or it’s a leaf with no children
**Rule of thumb:** If a component re-renders more often than its visible output
changes, add a selector (or a computed/small store). Use React DevTools
Profiler to confirm.