
React18 Batching Patterns
Fix silent React 18 automatic batching bugs in class components where this.state is read after await and stale values break conditional setState chains.
Overview
react18-batching-patterns is an agent skill for the Build phase that documents React 18 batching pitfalls and before/after fixes for class components reading this.state after await.
Install
npx skills add https://github.com/github/awesome-copilot --skill react18-batching-patternsWhat is this skill?
- Category A pattern: this.state read after await sees pre-update values under React 18 batching
- Before/after fixes for handleLoadClick-style loading flags that never clear
- Variant covers multi-step initialize chains where step guards read stale this.state
- Prescriptive fix: remove redundant conditions when you just set state true before await
- Documented silent-failure mode where setState after fetch never runs
- Documents Category A and Category A Variant (multi-step initialize) batching failure modes
Adoption & trust: 625 installs on skills.sh; 34.6k GitHub stars; 3/3 security scanners passed (skills.sh audits).
What problem does it solve?
Your class components silently stop updating after async work because this.state still reflects pre-await values when React 18 batches setState.
Who is it for?
Indie devs upgrading or debugging React 18 apps that still use class components and async handlers with conditional setState.
Skip if: Greenfield projects standardized on hooks with functional state updates only, or teams needing general concurrent rendering theory without class-component fixes.
When should I use this skill?
When debugging or migrating class components where setState before await and this.state checks after await cause missed updates under React 18 batching.
What do I get? / Deliverables
You refactor handlers to avoid stale this.state checks and use deterministic post-await setState so loading and data states flush reliably.
- Refactored async handlers without stale this.state guards
- Documented before/after snippets aligned to batching categories
Recommended Skills
Journey fit
Build frontend is the canonical shelf for React UI correctness during migration or maintenance of legacy class-based code. Batching after-await pitfalls directly affect component state handling in shipped user interfaces, not backend or ops surfaces.
How it compares
A targeted pattern cheatsheet for batching bugs—not a full Create React App migration skill or testing framework.
Common Questions / FAQ
Who is react18-batching-patterns for?
Solo frontend builders and maintainers of React class-based code who hit mysterious UI state bugs after moving to React 18.
When should I use react18-batching-patterns?
During Build frontend work when async handlers mis-set loading flags, and during Ship review when QA reports stuck spinners or skipped initialization steps.
Is react18-batching-patterns safe to install?
It is documentation-only pattern guidance with no runtime hooks; still review the Security Audits panel on this Prism page for the parent awesome-copilot package.
SKILL.md
READMESKILL.md - React18 Batching Patterns
# Batching Categories - Before/After Patterns ## Category A - this.state Read After Await (Silent Bug) {#category-a} The method reads `this.state` after an `await` to make a conditional decision. In React 18, the intermediate setState hasn't flushed yet - `this.state` still holds the pre-update value. **Before (broken in React 18):** ```jsx async handleLoadClick() { this.setState({ loading: true }); // batched - not flushed yet const data = await fetchData(); if (this.state.loading) { // ← still FALSE (old value) this.setState({ data, loading: false }); // ← never called } } ``` **After - remove the this.state read entirely:** ```jsx async handleLoadClick() { this.setState({ loading: true }); try { const data = await fetchData(); this.setState({ data, loading: false }); // always called - no condition needed } catch (err) { this.setState({ error: err, loading: false }); } } ``` **Pattern:** If the condition on `this.state` was always going to be true at that point (you just set it to true), remove the condition. The setState you called before `await` will eventually flush - you don't need to check it. --- ## Category A Variant - Multi-Step Conditional Chain ```jsx // Before (broken): async initialize() { this.setState({ step: 'auth' }); const token = await authenticate(); if (this.state.step === 'auth') { // ← wrong: still initial value this.setState({ step: 'loading', token }); const data = await loadData(token); if (this.state.step === 'loading') { // ← wrong again this.setState({ step: 'ready', data }); } } } ``` ```jsx // After - use local variables, not this.state, to track flow: async initialize() { this.setState({ step: 'auth' }); try { const token = await authenticate(); this.setState({ step: 'loading', token }); const data = await loadData(token); this.setState({ step: 'ready', data }); } catch (err) { this.setState({ step: 'error', error: err }); } } ``` --- ## Category B - Independent setState Calls (Refactor, No flushSync) {#category-b} Multiple setState calls in a Promise chain where order matters but no intermediate state reading occurs. The calls just need to be restructured. **Before:** ```jsx handleSubmit() { this.setState({ submitting: true }); submitForm(this.state.formData) .then(result => { this.setState({ result }); this.setState({ submitting: false }); // two setState in .then() }); } ``` **After - consolidate setState calls:** ```jsx async handleSubmit() { this.setState({ submitting: true, result: null, error: null }); try { const result = await submitForm(this.state.formData); this.setState({ result, submitting: false }); } catch (err) { this.setState({ error: err, submitting: false }); } } ``` Rule: Multiple `setState` calls in the same async context already batch in React 18. Consolidating into fewer calls is cleaner but not strictly required. --- ## Category C - Intermediate Render Must Be Visible (flushSync) {#category-c} The user must see an intermediate UI state (loading spinner, progress step) BEFORE an async operation starts. This is the only case where `flushSync` is the right answer. **Diagnostic question:** "If the loading spinner didn't appear until after the fetch returned, would the UX be wrong?" - YES → `flushSync` - NO → refactor (Category A or B) **Before:** ```jsx async processOrder() { this.setState({ status: 'validating' }); // user must see this await validateOrder(this.props.order); this.setState({ status: 'charging' }); // user must see this await chargeCard(this.props.card); this.setState({ status: 'complete' }); } ``` **After - flushSync for each required intermediate render:** ```jsx import { flushSync } from 'react-dom'; async processOrder() { flushSync(() => { this.setState({ status: 'validating' }); // renders immediately }); await validateOrder(this.props.