Skip to content

Commit 04697f7

Browse files
committed
Better useOptimistic and pending states.
1 parent d9f72f0 commit 04697f7

File tree

1 file changed

+74
-88
lines changed

1 file changed

+74
-88
lines changed

src/content/reference/react/useActionState.md

Lines changed: 74 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,13 @@ export default function Checkout() {
170170
<h2>Checkout</h2>
171171
<div className="row">
172172
<span>Eras Tour Tickets</span>
173-
<span>{isPending && '🌀 '}Qty: {count}</span>
173+
<span>Qty: {count}</span>
174174
</div>
175175
<div className="row">
176-
<button onClick={handleClick}>Add Ticket</button>
176+
<button onClick={handleClick}>Add Ticket{isPending ? ' 🌀' : ' '}</button>
177177
</div>
178178
<hr />
179-
<Total quantity={count} isPending={isPending} />
179+
<Total quantity={count} />
180180
</div>
181181
);
182182
}
@@ -232,6 +232,11 @@ export async function removeFromCart(count) {
232232
align-items: center;
233233
}
234234

235+
.row button {
236+
margin-left: auto;
237+
min-width: 150px;
238+
}
239+
235240
.total {
236241
font-weight: bold;
237242
}
@@ -303,16 +308,15 @@ export default function Checkout() {
303308
<div className="row">
304309
<span>Eras Tour Tickets</span>
305310
<span className="stepper">
306-
<span className="pending">{isPending && '🌀'}</span>
307-
<span className="qty">{count}</span>
311+
<span className="qty">{isPending ? '🌀' : count}</span>
308312
<span className="buttons">
309313
<button onClick={handleAdd}></button>
310314
<button onClick={handleRemove}></button>
311315
</span>
312316
</span>
313317
</div>
314318
<hr />
315-
<Total quantity={count} />
319+
<Total quantity={count} isPending={isPending}/>
316320
</div>
317321
);
318322
}
@@ -337,11 +341,11 @@ const formatter = new Intl.NumberFormat('en-US', {
337341
minimumFractionDigits: 0,
338342
});
339343

340-
export default function Total({quantity}) {
344+
export default function Total({quantity, isPending}) {
341345
return (
342346
<div className="row total">
343347
<span>Total</span>
344-
<span>{formatter.format(quantity * 9999)}</span>
348+
{isPending ? '🌀 Updating...' : formatter.format(quantity * 9999)}
345349
</div>
346350
);
347351
}
@@ -423,6 +427,10 @@ hr {
423427
424428
</Sandpack>
425429
430+
When you click to increase or decrease the quantity, an `"ADD"` or `"REMOVE"` is dispatched. In the `reducerAction`, different APIs are called to update the quantity.
431+
432+
In this example, we use the pending state of the Actions to replace both the quantity and the total. If you want to provide immediate feedback, such as immediately updating the quantity, you can use `useOptimistic`.
433+
426434
<DeepDive>
427435
428436
#### How is `useActionState` different from `useReducer`? {/*useactionstate-vs-usereducer*/}
@@ -439,44 +447,52 @@ You can think of `useActionState` as `useReducer` for side effects from user Act
439447
440448
---
441449
442-
### Using with Action props {/*using-with-action-props*/}
450+
### Using with `useOptimistic` {/*using-with-useoptimistic*/}
443451
444-
When you pass the `dispatchAction` function to a component that exposes an [Action prop](/reference/react/useTransition#exposing-action-props-from-components), you don't need to wrap the call in `startTransition` yourself.
452+
You can combine `useActionState` with [`useOptimistic`](/reference/react/useOptimistic) to show immediate UI feedback:
445453
446-
This example shows using the `increaseAction` and `decreaseAction` props of a QuantityStepper component:
447454
448455
<Sandpack>
449456
450457
```js src/App.js
451-
import { useActionState } from 'react';
458+
import { useActionState, startTransition, useOptimistic } from 'react';
452459
import { addToCart, removeFromCart } from './api';
453-
import QuantityStepper from './QuantityStepper';
454460
import Total from './Total';
455461

456462
export default function Checkout() {
457463
const [count, dispatchAction, isPending] = useActionState(updateCartAction, 0);
464+
const [optimisticCount, setOptimisticCount] = useOptimistic(count);
458465

459-
function addAction() {
460-
dispatchAction({type: 'ADD'});
466+
function handleAdd() {
467+
startTransition(() => {
468+
setOptimisticCount(c => c + 1);
469+
dispatchAction({ type: 'ADD' });
470+
});
461471
}
462472

463-
function removeAction() {
464-
dispatchAction({type: 'REMOVE'});
473+
function handleRemove() {
474+
startTransition(() => {
475+
setOptimisticCount(c => c - 1);
476+
dispatchAction({ type: 'REMOVE' });
477+
});
465478
}
466479

467480
return (
468481
<div className="checkout">
469482
<h2>Checkout</h2>
470483
<div className="row">
471484
<span>Eras Tour Tickets</span>
472-
<QuantityStepper
473-
value={count}
474-
increaseAction={addAction}
475-
decreaseAction={removeAction}
476-
/>
485+
<span className="stepper">
486+
<span className="pending">{isPending && '🌀'}</span>
487+
<span className="qty">{optimisticCount}</span>
488+
<span className="buttons">
489+
<button onClick={handleAdd}></button>
490+
<button onClick={handleRemove}></button>
491+
</span>
492+
</span>
477493
</div>
478494
<hr />
479-
<Total quantity={count} />
495+
<Total quantity={optimisticCount} isPending={isPending}/>
480496
</div>
481497
);
482498
}
@@ -494,49 +510,18 @@ async function updateCartAction(prevCount, actionPayload) {
494510
}
495511
```
496512
497-
```js src/QuantityStepper.js
498-
import { useTransition } from 'react';
499-
500-
export default function QuantityStepper({value, increaseAction, decreaseAction}) {
501-
const [isPending, startTransition] = useTransition();
502-
503-
function handleIncrease() {
504-
startTransition(async () => {
505-
await increaseAction();
506-
});
507-
}
508-
509-
function handleDecrease() {
510-
startTransition(async () => {
511-
await decreaseAction();
512-
});
513-
}
514-
515-
return (
516-
<span className="stepper">
517-
<span className="pending">{isPending && '🌀'}</span>
518-
<span className="qty">{value}</span>
519-
<span className="buttons">
520-
<button onClick={handleIncrease}></button>
521-
<button onClick={handleDecrease}></button>
522-
</span>
523-
</span>
524-
);
525-
}
526-
```
527-
528513
```js src/Total.js
529514
const formatter = new Intl.NumberFormat('en-US', {
530515
style: 'currency',
531516
currency: 'USD',
532517
minimumFractionDigits: 0,
533518
});
534519

535-
export default function Total({quantity}) {
520+
export default function Total({quantity, isPending}) {
536521
return (
537522
<div className="row total">
538523
<span>Total</span>
539-
<span>{formatter.format(quantity * 9999)}</span>
524+
<span>{isPending ? '🌀 Updating...' : formatter.format(quantity * 9999)}</span>
540525
</div>
541526
);
542527
}
@@ -618,47 +603,35 @@ hr {
618603
619604
</Sandpack>
620605
621-
Since `<QuantityStepper>` has built-in support for pending state, the loading indicator is shown automatically.
606+
607+
`setOptimisticCount` immediately updates the quantity, and `dispatchAction()` queues the `updateCartAction`. A pending indicator appears on both the quantity and total to give the user feedback that their update is still being applied.
622608
623609
---
624610
625-
### Using with `useOptimistic` {/*using-with-useoptimistic*/}
626611
627-
You can combine `useActionState` with [`useOptimistic`](/reference/react/useOptimistic) to show immediate UI feedback:
612+
### Using with Action props {/*using-with-action-props*/}
628613
614+
When you pass the `dispatchAction` function to a component that exposes an [Action prop](/reference/react/useTransition#exposing-action-props-from-components), you don't need to call `startTransition` or `useOptmisitc` yourself.
615+
616+
This example shows using the `increaseAction` and `decreaseAction` props of a QuantityStepper component:
629617
630618
<Sandpack>
631619
632620
```js src/App.js
633-
import { useActionState, useOptimistic } from 'react';
621+
import { useActionState } from 'react';
634622
import { addToCart, removeFromCart } from './api';
635623
import QuantityStepper from './QuantityStepper';
636624
import Total from './Total';
637625

638-
async function updateCartAction(prevCount, actionPayload) {
639-
switch (actionPayload.type) {
640-
case 'ADD': {
641-
return await addToCart(prevCount);
642-
}
643-
case 'REMOVE': {
644-
return await removeFromCart(prevCount);
645-
}
646-
}
647-
return prevCount;
648-
}
649-
650626
export default function Checkout() {
651627
const [count, dispatchAction, isPending] = useActionState(updateCartAction, 0);
652-
const [optimisticCount, setOptimisticCount] = useOptimistic(count);
653628

654-
async function addAction() {
655-
setOptimisticCount(c => c + 1);
656-
await dispatchAction({type: 'ADD'});
629+
function addAction() {
630+
dispatchAction({type: 'ADD'});
657631
}
658632

659-
async function removeAction() {
660-
setOptimisticCount(c => Math.max(0, c - 1));
661-
await dispatchAction({type: 'REMOVE'});
633+
function removeAction() {
634+
dispatchAction({type: 'REMOVE'});
662635
}
663636

664637
return (
@@ -667,40 +640,54 @@ export default function Checkout() {
667640
<div className="row">
668641
<span>Eras Tour Tickets</span>
669642
<QuantityStepper
670-
value={optimisticCount}
643+
value={count}
671644
increaseAction={addAction}
672645
decreaseAction={removeAction}
673646
/>
674647
</div>
675648
<hr />
676-
<Total quantity={optimisticCount} isPending={isPending} />
649+
<Total quantity={count} isPending={isPending} />
677650
</div>
678651
);
679652
}
653+
654+
async function updateCartAction(prevCount, actionPayload) {
655+
switch (actionPayload.type) {
656+
case 'ADD': {
657+
return await addToCart(prevCount);
658+
}
659+
case 'REMOVE': {
660+
return await removeFromCart(prevCount);
661+
}
662+
}
663+
return prevCount;
664+
}
680665
```
681666
682667
```js src/QuantityStepper.js
683-
import { useTransition } from 'react';
668+
import { startTransition, useOptimistic } from 'react';
684669

685670
export default function QuantityStepper({value, increaseAction, decreaseAction}) {
686-
const [isPending, startTransition] = useTransition();
687-
671+
const [optimisticValue, setOptimisticValue] = useOptimistic(value);
672+
const isPending = value !== optimisticValue;
688673
function handleIncrease() {
689674
startTransition(async () => {
675+
setOptimisticValue(c => c + 1);
690676
await increaseAction();
691677
});
692678
}
693679

694680
function handleDecrease() {
695681
startTransition(async () => {
682+
setOptimisticValue(c => c + 1);
696683
await decreaseAction();
697684
});
698685
}
699686

700687
return (
701688
<span className="stepper">
702689
<span className="pending">{isPending && '🌀'}</span>
703-
<span className="qty">{value}</span>
690+
<span className="qty">{optimisticValue}</span>
704691
<span className="buttons">
705692
<button onClick={handleIncrease}></button>
706693
<button onClick={handleDecrease}></button>
@@ -721,7 +708,7 @@ export default function Total({quantity, isPending}) {
721708
return (
722709
<div className="row total">
723710
<span>Total</span>
724-
<span>{isPending ? '🌀' : ''}{formatter.format(quantity * 9999)}</span>
711+
{isPending ? '🌀 Updating...' : formatter.format(quantity * 9999)}
725712
</div>
726713
);
727714
}
@@ -803,8 +790,7 @@ hr {
803790
804791
</Sandpack>
805792
806-
807-
When the stepper arrow is clicked, `setOptimisticCount` immediately updates the quantity, and `dispatchAction()` queues the `updateCartAction`. A pending indicator appears on both the quantity and total to give the user feedback that their update is still being applied.
793+
Since `<QuantityStepper>` has built-in support for transitions, pending state, and optimistically updating the count, you just need to tell the Action _what_ to change, and _how_ to change it is handled for you.
808794
809795
---
810796

0 commit comments

Comments
 (0)