Skip to content

Commit 491aec5

Browse files
authored
Implement experimental_useOptimisticState (#26740)
This adds an experimental hook tentatively called useOptimisticState. (The actual name needs some bikeshedding.) The headline feature is that you can use it to implement optimistic updates. If you set some optimistic state during a transition/action, the state will be automatically reverted once the transition completes. Another feature is that the optimistic updates will be continually rebased on top of the latest state. It's easiest to explain with examples; we'll publish documentation as the API gets closer to stabilizing. See tests for now. Technically the use cases for this hook are broader than just optimistic updates; you could use it implement any sort of "pending" state, such as the ones exposed by useTransition and useFormStatus. But we expect people will most often reach for this hook to implement the optimistic update pattern; simpler cases are covered by those other hooks.
1 parent 9545e48 commit 491aec5

File tree

13 files changed

+846
-66
lines changed

13 files changed

+846
-66
lines changed

packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,10 +1243,9 @@ describe('Timeline profiler', () => {
12431243
function Example() {
12441244
const setHigh = React.useState(0)[1];
12451245
const setLow = React.useState(0)[1];
1246-
const startTransition = React.useTransition()[1];
12471246

12481247
updaterFn = () => {
1249-
startTransition(() => {
1248+
React.startTransition(() => {
12501249
setLow(prevLow => prevLow + 1);
12511250
});
12521251
setHigh(prevHigh => prevHigh + 1);
@@ -1265,24 +1264,6 @@ describe('Timeline profiler', () => {
12651264
const timelineData = stopProfilingAndGetTimelineData();
12661265
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
12671266
[
1268-
{
1269-
"componentName": "Example",
1270-
"componentStack": "
1271-
in Example (at **)",
1272-
"lanes": "0b0000000000000000000000000001000",
1273-
"timestamp": 10,
1274-
"type": "schedule-state-update",
1275-
"warning": null,
1276-
},
1277-
{
1278-
"componentName": "Example",
1279-
"componentStack": "
1280-
in Example (at **)",
1281-
"lanes": "0b0000000000000000000000010000000",
1282-
"timestamp": 10,
1283-
"type": "schedule-state-update",
1284-
"warning": null,
1285-
},
12861267
{
12871268
"componentName": "Example",
12881269
"componentStack": "

packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ let React;
2222
let ReactDOMServer;
2323
let ReactDOMClient;
2424
let useFormStatus;
25+
let useOptimisticState;
2526

2627
describe('ReactDOMFizzForm', () => {
2728
beforeEach(() => {
@@ -30,6 +31,7 @@ describe('ReactDOMFizzForm', () => {
3031
ReactDOMServer = require('react-dom/server.browser');
3132
ReactDOMClient = require('react-dom/client');
3233
useFormStatus = require('react-dom').experimental_useFormStatus;
34+
useOptimisticState = require('react').experimental_useOptimisticState;
3335
act = require('internal-test-utils').act;
3436
container = document.createElement('div');
3537
document.body.appendChild(container);
@@ -453,4 +455,21 @@ describe('ReactDOMFizzForm', () => {
453455
expect(deletedTitle).toBe('Hello');
454456
expect(rootActionCalled).toBe(false);
455457
});
458+
459+
// @gate enableAsyncActions
460+
it('useOptimisticState returns passthrough value', async () => {
461+
function App() {
462+
const [optimisticState] = useOptimisticState('hi');
463+
return optimisticState;
464+
}
465+
466+
const stream = await ReactDOMServer.renderToReadableStream(<App />);
467+
await readIntoContainer(stream);
468+
expect(container.textContent).toBe('hi');
469+
470+
await act(async () => {
471+
ReactDOMClient.hydrateRoot(container, <App />);
472+
});
473+
expect(container.textContent).toBe('hi');
474+
});
456475
});

0 commit comments

Comments
 (0)