Skip to content

Commit eb89912

Browse files
authored
Add expertimental optimisticKey behind a flag (facebook#35162)
When dealing with optimistic state, a common problem is not knowing the id of the thing we're waiting on. Items in lists need keys (and single items should often have keys too to reset their state). As a result you have to generate fake keys. It's a pain to manage those and when the real item comes in, you often end up rendering that with a different `key` which resets the state of the component tree. That in turns works against the grain of React and a lot of negatives fall out of it. This adds a special `optimisticKey` symbol that can be used in place of a `string` key. ```js import {optimisticKey} from 'react'; ... const [optimisticItems, setOptimisticItems] = useOptimistic([]); const children = savedItems.concat( optimisticItems.map(item => <Item key={optimisticKey} item={item} /> ) ); return <div>{children}</div>; ``` The semantics of this `optimisticKey` is that the assumption is that the newly saved item will be rendered in the same slot as the previous optimistic items. State is transferred into whatever real key ends up in the same slot. This might lead to some incorrect transferring of state in some cases where things don't end up lining up - but it's worth it for simplicity in many cases since dealing with true matching of optimistic state is often very complex for something that only lasts a blink of an eye. If a new item matches a `key` elsewhere in the set, then that's favored over reconciling against the old slot. One quirk with the current algorithm is if the `savedItems` has items removed, then the slots won't line up by index anymore and will be skewed. We might be able to add something where the optimistic set is always reconciled against the end. However, it's probably better to just assume that the set will line up perfectly and otherwise it's just best effort that can lead to weird artifacts. An `optimisticKey` will match itself for updates to the same slot, but it will not match any existing slot that is not an `optimisticKey`. So it's not an `any`, which I originally called it, because it doesn't match existing real keys against new optimistic keys. Only one direction.
1 parent 0972e23 commit eb89912

27 files changed

+454
-83
lines changed

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3884,4 +3884,19 @@ describe('ReactFlight', () => {
38843884
</main>,
38853885
);
38863886
});
3887+
3888+
// @gate enableOptimisticKey
3889+
it('collapses optimistic keys to an optimistic key', async () => {
3890+
function Bar({text}) {
3891+
return <div />;
3892+
}
3893+
function Foo() {
3894+
return <Bar key={ReactServer.optimisticKey} />;
3895+
}
3896+
const transport = ReactNoopFlightServer.render({
3897+
element: <Foo key="Outer Key" />,
3898+
});
3899+
const model = await ReactNoopFlightClient.read(transport);
3900+
expect(model.element.key).toBe(React.optimisticKey);
3901+
});
38873902
});

packages/react-devtools-shared/src/backend/fiber/renderer.js

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ import {
120120
MEMO_SYMBOL_STRING,
121121
SERVER_CONTEXT_SYMBOL_STRING,
122122
LAZY_SYMBOL_STRING,
123+
REACT_OPTIMISTIC_KEY,
123124
} from '../shared/ReactSymbols';
124125
import {enableStyleXFeatures} from 'react-devtools-feature-flags';
125126

@@ -4849,7 +4850,10 @@ export function attach(
48494850
}
48504851
let previousSiblingOfBestMatch = null;
48514852
let bestMatch = remainingReconcilingChildren;
4852-
if (componentInfo.key != null) {
4853+
if (
4854+
componentInfo.key != null &&
4855+
componentInfo.key !== REACT_OPTIMISTIC_KEY
4856+
) {
48534857
// If there is a key try to find a matching key in the set.
48544858
bestMatch = remainingReconcilingChildren;
48554859
while (bestMatch !== null) {
@@ -6145,7 +6149,7 @@ export function attach(
61456149
return {
61466150
displayName: getDisplayNameForFiber(fiber) || 'Anonymous',
61476151
id: instance.id,
6148-
key: fiber.key,
6152+
key: fiber.key === REACT_OPTIMISTIC_KEY ? null : fiber.key,
61496153
env: null,
61506154
stack:
61516155
fiber._debugOwner == null || fiber._debugStack == null
@@ -6158,7 +6162,11 @@ export function attach(
61586162
return {
61596163
displayName: componentInfo.name || 'Anonymous',
61606164
id: instance.id,
6161-
key: componentInfo.key == null ? null : componentInfo.key,
6165+
key:
6166+
componentInfo.key == null ||
6167+
componentInfo.key === REACT_OPTIMISTIC_KEY
6168+
? null
6169+
: componentInfo.key,
61626170
env: componentInfo.env == null ? null : componentInfo.env,
61636171
stack:
61646172
componentInfo.owner == null || componentInfo.debugStack == null
@@ -7082,7 +7090,7 @@ export function attach(
70827090
// Does the component have legacy context attached to it.
70837091
hasLegacyContext,
70847092
7085-
key: key != null ? key : null,
7093+
key: key != null && key !== REACT_OPTIMISTIC_KEY ? key : null,
70867094
70877095
type: elementType,
70887096
@@ -8641,15 +8649,19 @@ export function attach(
86418649
}
86428650
return {
86438651
displayName,
8644-
key,
8652+
key: key === REACT_OPTIMISTIC_KEY ? null : key,
86458653
index,
86468654
};
86478655
}
86488656

86498657
function getVirtualPathFrame(virtualInstance: VirtualInstance): PathFrame {
86508658
return {
86518659
displayName: virtualInstance.data.name || '',
8652-
key: virtualInstance.data.key == null ? null : virtualInstance.data.key,
8660+
key:
8661+
virtualInstance.data.key == null ||
8662+
virtualInstance.data.key === REACT_OPTIMISTIC_KEY
8663+
? null
8664+
: virtualInstance.data.key,
86538665
index: -1, // We use -1 to indicate that this is a virtual path frame.
86548666
};
86558667
}

packages/react-devtools-shared/src/backend/shared/ReactSymbols.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,9 @@ export const SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED_SYMBOL_STRING =
7272
export const REACT_MEMO_CACHE_SENTINEL: symbol = Symbol.for(
7373
'react.memo_cache_sentinel',
7474
);
75+
76+
import type {ReactOptimisticKey} from 'shared/ReactTypes';
77+
78+
export const REACT_OPTIMISTIC_KEY: ReactOptimisticKey = (Symbol.for(
79+
'react.optimistic_key',
80+
): any);

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,4 +1111,64 @@ describe('ReactDOMFizzStaticBrowser', () => {
11111111
</div>,
11121112
);
11131113
});
1114+
1115+
// @gate enableHalt && enableOptimisticKey
1116+
it('can resume an optimistic keyed slot', async () => {
1117+
const errors = [];
1118+
1119+
let resolve;
1120+
const promise = new Promise(r => (resolve = r));
1121+
1122+
async function Component() {
1123+
await promise;
1124+
return 'Hi';
1125+
}
1126+
1127+
if (React.optimisticKey === undefined) {
1128+
throw new Error('optimisticKey missing');
1129+
}
1130+
1131+
function App() {
1132+
return (
1133+
<div>
1134+
<Suspense fallback="Loading">
1135+
<Component key={React.optimisticKey} />
1136+
</Suspense>
1137+
</div>
1138+
);
1139+
}
1140+
1141+
const controller = new AbortController();
1142+
const pendingResult = serverAct(() =>
1143+
ReactDOMFizzStatic.prerender(<App />, {
1144+
signal: controller.signal,
1145+
onError(x) {
1146+
errors.push(x.message);
1147+
},
1148+
}),
1149+
);
1150+
1151+
await serverAct(() => {
1152+
controller.abort();
1153+
});
1154+
1155+
const prerendered = await pendingResult;
1156+
1157+
const postponedState = JSON.stringify(prerendered.postponed);
1158+
1159+
await readIntoContainer(prerendered.prelude);
1160+
expect(getVisibleChildren(container)).toEqual(<div>Loading</div>);
1161+
1162+
expect(prerendered.postponed).not.toBe(null);
1163+
1164+
await resolve();
1165+
1166+
const dynamic = await serverAct(() =>
1167+
ReactDOMFizzServer.resume(<App />, JSON.parse(postponedState)),
1168+
);
1169+
1170+
await readIntoContainer(dynamic);
1171+
1172+
expect(getVisibleChildren(container)).toEqual(<div>Hi</div>);
1173+
});
11141174
});

0 commit comments

Comments
 (0)