Files
stack/packages/mosaic/framework/skills/vercel-react-native-skills/rules/react-state-dispatcher.md
T
fargo 1a822493ba format: apply repo prettier (3.8.1) to the folded skills tree
963 markdown files reformatted with the repository's pinned prettier so
pnpm format:check covers the folded tree like every other repo file.

The formatter's embedded-language pass also normalized code fences
(TS semicolons, closed HTML tags in examples, lowercased CSS hex colors,
one renumbered list that skipped an index). Alphanumeric token deltas vs
the fold commit were audited file-by-file; all are formatter-equivalent
markup normalizations plus the four sanitized skills.
2026-08-19 14:37:17 -05:00

2.2 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
useState Dispatch updaters for State That Depends on Current Value MEDIUM avoids stale closures, prevents unnecessary re-renders state, hooks, useState, callbacks

Use Dispatch Updaters for State That Depends on Current Value

When the next state depends on the current state, use a dispatch updater (setState(prev => ...)) instead of reading the state variable directly in a callback. This avoids stale closures and ensures you're comparing against the latest value.

Incorrect (reads state directly):

const [size, setSize] = useState<Size | undefined>(undefined);

const onLayout = (e: LayoutChangeEvent) => {
  const { width, height } = e.nativeEvent.layout;
  // size may be stale in this closure
  if (size?.width !== width || size?.height !== height) {
    setSize({ width, height });
  }
};

Correct (dispatch updater):

const [size, setSize] = useState<Size | undefined>(undefined);

const onLayout = (e: LayoutChangeEvent) => {
  const { width, height } = e.nativeEvent.layout;
  setSize((prev) => {
    if (prev?.width === width && prev?.height === height) return prev;
    return { width, height };
  });
};

Returning the previous value from the updater skips the re-render.

For primitive states, you don't need to compare values before firing a re-render.

Incorrect (unnecessary comparison for primitive state):

const [size, setSize] = useState<Size | undefined>(undefined);

const onLayout = (e: LayoutChangeEvent) => {
  const { width, height } = e.nativeEvent.layout;
  setSize((prev) => (prev === width ? prev : width));
};

Correct (sets primitive state directly):

const [size, setSize] = useState<Size | undefined>(undefined);

const onLayout = (e: LayoutChangeEvent) => {
  const { width, height } = e.nativeEvent.layout;
  setSize(width);
};

However, if the next state depends on the current state, you should still use a dispatch updater.

Incorrect (reads state directly from the callback):

const [count, setCount] = useState(0);

const onTap = () => {
  setCount(count + 1);
};

Correct (dispatch updater):

const [count, setCount] = useState(0);

const onTap = () => {
  setCount((prev) => prev + 1);
};