mirror of
https://github.com/github/awesome-copilot.git
synced 2026-04-12 19:25:55 +00:00
chore: publish from staged
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
# 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(<AutoSaveForm />);
|
||||
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(<AutoSaveForm />);
|
||||
fireEvent.change(input, { target: { value: 'new text' } });
|
||||
await waitFor(() => expect(screen.getByText('Saving...')).toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.getByText('Saved')).toBeInTheDocument());
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,86 @@
|
||||
# flushSync Guide
|
||||
|
||||
## Import
|
||||
|
||||
```jsx
|
||||
import { flushSync } from 'react-dom';
|
||||
// NOT from 'react' - it lives in react-dom
|
||||
```
|
||||
|
||||
If the file already imports from `react-dom`:
|
||||
|
||||
```jsx
|
||||
import ReactDOM from 'react-dom';
|
||||
// Add named import:
|
||||
import ReactDOM, { flushSync } from 'react-dom';
|
||||
```
|
||||
|
||||
## Syntax
|
||||
|
||||
```jsx
|
||||
flushSync(() => {
|
||||
this.setState({ ... });
|
||||
});
|
||||
// After this line, the re-render has completed synchronously
|
||||
```
|
||||
|
||||
Multiple setState calls inside one flushSync batch together into ONE synchronous render:
|
||||
|
||||
```jsx
|
||||
flushSync(() => {
|
||||
this.setState({ step: 'loading' });
|
||||
this.setState({ progress: 0 });
|
||||
// These batch together → one render
|
||||
});
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
||||
✅ Use when the user must see a specific UI state BEFORE an async operation starts:
|
||||
|
||||
```jsx
|
||||
flushSync(() => this.setState({ loading: true }));
|
||||
await expensiveAsyncOperation();
|
||||
```
|
||||
|
||||
✅ Use in multi-step progress flows where each step must visually complete before the next:
|
||||
|
||||
```jsx
|
||||
flushSync(() => this.setState({ status: 'validating' }));
|
||||
await validate();
|
||||
flushSync(() => this.setState({ status: 'processing' }));
|
||||
await process();
|
||||
```
|
||||
|
||||
✅ Use in tests that must assert an intermediate UI state synchronously (avoid when possible - prefer `waitFor`).
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
❌ Don't use it to "fix" a reading-this.state-after-await bug - that's Category A (refactor instead):
|
||||
|
||||
```jsx
|
||||
// WRONG - flushSync doesn't fix this
|
||||
flushSync(() => this.setState({ loading: true }));
|
||||
const data = await fetchData();
|
||||
if (this.state.loading) { ... } // still a race condition
|
||||
```
|
||||
|
||||
❌ Don't use it for every setState to "be safe" - it defeats React 18 concurrent rendering:
|
||||
|
||||
```jsx
|
||||
// WRONG - excessive flushSync
|
||||
async handleClick() {
|
||||
flushSync(() => this.setState({ clicked: true })); // unnecessary
|
||||
flushSync(() => this.setState({ processing: true })); // unnecessary
|
||||
const result = await doWork();
|
||||
flushSync(() => this.setState({ result, done: true })); // unnecessary
|
||||
}
|
||||
```
|
||||
|
||||
❌ Don't use it inside a `useEffect` or `componentDidMount` to trigger immediate state - it causes nested render cycles.
|
||||
|
||||
## Performance Note
|
||||
|
||||
`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.
|
||||
|
||||
If you find yourself adding more than 2 `flushSync` calls to a single method, reconsider whether the component's state model needs redesign.
|
||||
Reference in New Issue
Block a user