# 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.order); flushSync(() => { this.setState({ status: 'charging' }); // renders immediately }); await chargeCard(this.props.card); this.setState({ status: 'complete' }); // last - no flushSync needed } ``` **Simple loading spinner case** (most common): ```jsx import { flushSync } from 'react-dom'; async handleSearch() { // User must see spinner before the fetch begins flushSync(() => this.setState({ loading: true })); const results = await searchAPI(this.state.query); this.setState({ results, loading: false }); } ``` --- ## setTimeout Pattern ```jsx // Before (React 17 - setTimeout fired immediate re-renders): handleAutoSave() { setTimeout(() => { this.setState({ saving: true }); // React 17: re-render happened here saveToServer(this.state.formData).then(() => { this.setState({ saving: false, lastSaved: Date.now() }); }); }, 2000); } ``` ```jsx // After (React 18 - all setState inside setTimeout batches): handleAutoSave() { setTimeout(async () => { // If loading state must show before fetch - flushSync flushSync(() => this.setState({ saving: true })); await saveToServer(this.state.formData); this.setState({ saving: false, lastSaved: Date.now() }); }, 2000); } ``` --- ## Test Patterns That Break Due to Batching ```jsx // Before (React 17 - intermediate state was synchronously visible): it('shows saving indicator', () => { render(); fireEvent.change(input, { target: { value: 'new text' } }); expect(screen.getByText('Saving...')).toBeInTheDocument(); // ← sync check }); // After (React 18 - use waitFor for intermediate states): it('shows saving indicator', async () => { render(); fireEvent.change(input, { target: { value: 'new text' } }); await waitFor(() => expect(screen.getByText('Saving...')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('Saved')).toBeInTheDocument()); }); ```