{"kind":"Skill","metadata":{"namespace":"community","name":"react18-batching-patterns","version":"0.1.0"},"spec":{"description":"Provides exact patterns for diagnosing and fixing automatic batching regressions in React 18 class components. Use this skill whenever a class component has multiple setState calls in an async method, inside setTimeout, inside a Promise .then() or .catch(), or in a native event handler. Use it before writing any flushSync call - the decision tree here prevents unnecessary flushSync overuse. Also use this skill when fixing test failures caused by intermediate state assertions that break after React 18 upgrade.","files":{"SKILL.md":"---\nname: react18-batching-patterns\ndescription: 'Provides exact patterns for diagnosing and fixing automatic batching regressions in React 18 class components. Use this skill whenever a class component has multiple setState calls in an async method, inside setTimeout, inside a Promise .then() or .catch(), or in a native event handler. Use it before writing any flushSync call - the decision tree here prevents unnecessary flushSync overuse. Also use this skill when fixing test failures caused by intermediate state assertions that break after React 18 upgrade.'\n---\n\n# React 18 Automatic Batching Patterns\n\nReference for diagnosing and fixing the most dangerous silent breaking change in React 18 for class-component codebases.\n\n## The Core Change\n\n| Location of setState | React 17 | React 18 |\n|---|---|---|\n| React event handler | Batched | Batched (same) |\n| setTimeout | **Immediate re-render** | **Batched** |\n| Promise .then() / .catch() | **Immediate re-render** | **Batched** |\n| async/await | **Immediate re-render** | **Batched** |\n| Native addEventListener callback | **Immediate re-render** | **Batched** |\n\n**Batched** means: all setState calls within that execution context flush together in a single re-render at the end. No intermediate renders occur.\n\n## Quick Diagnosis\n\nRead every async class method. Ask: does any code after an `await` read `this.state` to make a decision?\n\n```\nCode reads this.state after await?\n  YES → Category A (silent state-read bug)\n  NO, but intermediate render must be visible to user?\n    YES → Category C (flushSync needed)\n    NO → Category B (refactor, no flushSync)\n```\n\nFor the full pattern for each category, read:\n- **`references/batching-categories.md`** - Category A, B, C with full before/after code\n- **`references/flushSync-guide.md`** - when to use flushSync, when NOT to, import syntax\n\n## The flushSync Rule\n\n**Use `flushSync` sparingly.** It forces a synchronous re-render, bypassing React 18's concurrent scheduler. Overusing it negates the performance benefits of React 18.\n\nOnly use `flushSync` when:\n- The user must see an intermediate UI state before an async operation begins\n- A spinner/loading state must render before a fetch starts\n- Sequential UI steps have distinct visible states (progress wizard, multi-step flow)\n\nIn most cases, the fix is a **refactor** - restructuring the code to not read `this.state` after `await`. Read `references/batching-categories.md` for the correct approach per category.\n","references/batching-categories.md":"# Batching Categories - Before/After Patterns\n\n## Category A - this.state Read After Await (Silent Bug) {#category-a}\n\nThe 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.\n\n**Before (broken in React 18):**\n\n```jsx\nasync handleLoadClick() {\n  this.setState({ loading: true });       // batched - not flushed yet\n  const data = await fetchData();\n  if (this.state.loading) {               // ← still FALSE (old value)\n    this.setState({ data, loading: false });  // ← never called\n  }\n}\n```\n\n**After - remove the this.state read entirely:**\n\n```jsx\nasync handleLoadClick() {\n  this.setState({ loading: true });\n  try {\n    const data = await fetchData();\n    this.setState({ data, loading: false }); // always called - no condition needed\n  } catch (err) {\n    this.setState({ error: err, loading: false });\n  }\n}\n```\n\n**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.\n\n---\n\n## Category A Variant - Multi-Step Conditional Chain\n\n```jsx\n// Before (broken):\nasync initialize() {\n  this.setState({ step: 'auth' });\n  const token = await authenticate();\n  if (this.state.step === 'auth') {        // ← wrong: still initial value\n    this.setState({ step: 'loading', token });\n    const data = await loadData(token);\n    if (this.state.step === 'loading') {   // ← wrong again\n      this.setState({ step: 'ready', data });\n    }\n  }\n}\n```\n\n```jsx\n// After - use local variables, not this.state, to track flow:\nasync initialize() {\n  this.setState({ step: 'auth' });\n  try {\n    const token = await authenticate();\n    this.setState({ step: 'loading', token });\n    const data = await loadData(token);\n    this.setState({ step: 'ready', data });\n  } catch (err) {\n    this.setState({ step: 'error', error: err });\n  }\n}\n```\n\n---\n\n## Category B - Independent setState Calls (Refactor, No flushSync) {#category-b}\n\nMultiple setState calls in a Promise chain where order matters but no intermediate state reading occurs. The calls just need to be restructured.\n\n**Before:**\n\n```jsx\nhandleSubmit() {\n  this.setState({ submitting: true });\n  submitForm(this.state.formData)\n    .then(result =\u003e {\n      this.setState({ result });\n      this.setState({ submitting: false });  // two setState in .then()\n    });\n}\n```\n\n**After - consolidate setState calls:**\n\n```jsx\nasync handleSubmit() {\n  this.setState({ submitting: true, result: null, error: null });\n  try {\n    const result = await submitForm(this.state.formData);\n    this.setState({ result, submitting: false });\n  } catch (err) {\n    this.setState({ error: err, submitting: false });\n  }\n}\n```\n\nRule: Multiple `setState` calls in the same async context already batch in React 18. Consolidating into fewer calls is cleaner but not strictly required.\n\n---\n\n## Category C - Intermediate Render Must Be Visible (flushSync) {#category-c}\n\nThe 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.\n\n**Diagnostic question:** \"If the loading spinner didn't appear until after the fetch returned, would the UX be wrong?\"\n\n- YES → `flushSync`\n- NO → refactor (Category A or B)\n\n**Before:**\n\n```jsx\nasync processOrder() {\n  this.setState({ status: 'validating' });   // user must see this\n  await validateOrder(this.props.order);\n  this.setState({ status: 'charging' });     // user must see this\n  await chargeCard(this.props.card);\n  this.setState({ status: 'complete' });\n}\n```\n\n**After - flushSync for each required intermediate render:**\n\n```jsx\nimport { flushSync } from 'react-dom';\n\nasync processOrder() {\n  flushSync(() =\u003e {\n    this.setState({ status: 'validating' });  // renders immediately\n  });\n  await validateOrder(this.props.order);\n\n  flushSync(() =\u003e {\n    this.setState({ status: 'charging' });    // renders immediately\n  });\n  await chargeCard(this.props.card);\n\n  this.setState({ status: 'complete' });      // last - no flushSync needed\n}\n```\n\n**Simple loading spinner case** (most common):\n\n```jsx\nimport { flushSync } from 'react-dom';\n\nasync handleSearch() {\n  // User must see spinner before the fetch begins\n  flushSync(() =\u003e this.setState({ loading: true }));\n  const results = await searchAPI(this.state.query);\n  this.setState({ results, loading: false });\n}\n```\n\n---\n\n## setTimeout Pattern\n\n```jsx\n// Before (React 17 - setTimeout fired immediate re-renders):\nhandleAutoSave() {\n  setTimeout(() =\u003e {\n    this.setState({ saving: true });\n    // React 17: re-render happened here\n    saveToServer(this.state.formData).then(() =\u003e {\n      this.setState({ saving: false, lastSaved: Date.now() });\n    });\n  }, 2000);\n}\n```\n\n```jsx\n// After (React 18 - all setState inside setTimeout batches):\nhandleAutoSave() {\n  setTimeout(async () =\u003e {\n    // If loading state must show before fetch - flushSync\n    flushSync(() =\u003e this.setState({ saving: true }));\n    await saveToServer(this.state.formData);\n    this.setState({ saving: false, lastSaved: Date.now() });\n  }, 2000);\n}\n```\n\n---\n\n## Test Patterns That Break Due to Batching\n\n```jsx\n// Before (React 17 - intermediate state was synchronously visible):\nit('shows saving indicator', () =\u003e {\n  render(\u003cAutoSaveForm /\u003e);\n  fireEvent.change(input, { target: { value: 'new text' } });\n  expect(screen.getByText('Saving...')).toBeInTheDocument(); // ← sync check\n});\n\n// After (React 18 - use waitFor for intermediate states):\nit('shows saving indicator', async () =\u003e {\n  render(\u003cAutoSaveForm /\u003e);\n  fireEvent.change(input, { target: { value: 'new text' } });\n  await waitFor(() =\u003e expect(screen.getByText('Saving...')).toBeInTheDocument());\n  await waitFor(() =\u003e expect(screen.getByText('Saved')).toBeInTheDocument());\n});\n```\n","references/flushSync-guide.md":"# flushSync Guide\n\n## Import\n\n```jsx\nimport { flushSync } from 'react-dom';\n// NOT from 'react' - it lives in react-dom\n```\n\nIf the file already imports from `react-dom`:\n\n```jsx\nimport ReactDOM from 'react-dom';\n// Add named import:\nimport ReactDOM, { flushSync } from 'react-dom';\n```\n\n## Syntax\n\n```jsx\nflushSync(() =\u003e {\n  this.setState({ ... });\n});\n// After this line, the re-render has completed synchronously\n```\n\nMultiple setState calls inside one flushSync batch together into ONE synchronous render:\n\n```jsx\nflushSync(() =\u003e {\n  this.setState({ step: 'loading' });\n  this.setState({ progress: 0 });\n  // These batch together → one render\n});\n```\n\n## When to Use\n\n✅ Use when the user must see a specific UI state BEFORE an async operation starts:\n\n```jsx\nflushSync(() =\u003e this.setState({ loading: true }));\nawait expensiveAsyncOperation();\n```\n\n✅ Use in multi-step progress flows where each step must visually complete before the next:\n\n```jsx\nflushSync(() =\u003e this.setState({ status: 'validating' }));\nawait validate();\nflushSync(() =\u003e this.setState({ status: 'processing' }));\nawait process();\n```\n\n✅ Use in tests that must assert an intermediate UI state synchronously (avoid when possible - prefer `waitFor`).\n\n## When NOT to Use\n\n❌ Don't use it to \"fix\" a reading-this.state-after-await bug - that's Category A (refactor instead):\n\n```jsx\n// WRONG - flushSync doesn't fix this\nflushSync(() =\u003e this.setState({ loading: true }));\nconst data = await fetchData();\nif (this.state.loading) { ... } // still a race condition\n```\n\n❌ Don't use it for every setState to \"be safe\" - it defeats React 18 concurrent rendering:\n\n```jsx\n// WRONG - excessive flushSync\nasync handleClick() {\n  flushSync(() =\u003e this.setState({ clicked: true }));   // unnecessary\n  flushSync(() =\u003e this.setState({ processing: true })); // unnecessary\n  const result = await doWork();\n  flushSync(() =\u003e this.setState({ result, done: true })); // unnecessary\n}\n```\n\n❌ Don't use it inside a `useEffect` or `componentDidMount` to trigger immediate state - it causes nested render cycles.\n\n## Performance Note\n\n`flushSync` forces a synchronous render, which blocks the browser thread until the render completes. On slow devices or complex component trees, multiple `flushSync` calls in an async method will cause visible jank. Use sparingly.\n\nIf you find yourself adding more than 2 `flushSync` calls to a single method, reconsider whether the component's state model needs redesign.\n"},"import":{"commit_sha":"541b7819d8c3545c6df122491af4fa1eae415779","imported_at":"2026-05-18T20:05:35Z","license_text":"MIT License\n\nCopyright GitHub, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.","owner":"github","repo":"github/awesome-copilot","source_url":"https://github.com/github/awesome-copilot/tree/541b7819d8c3545c6df122491af4fa1eae415779/plugins/react18-upgrade/skills/react18-batching-patterns"}},"content_hash":[15,30,149,58,149,91,230,19,233,226,14,148,68,157,34,195,132,85,236,50,181,223,99,25,222,205,68,235,222,32,93,147],"trust_level":"unsigned","yanked":false}
