Skip to content

Commit eb89912

Browse files
authored
Add expertimental optimisticKey behind a flag (#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

Lines changed: 454 additions & 83 deletions

‎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+
functionBar({text}){
3891+
return<div/>;
3892+
}
3893+
functionFoo(){
3894+
return<Barkey={ReactServer.optimisticKey}/>;
3895+
}
3896+
consttransport=ReactNoopFlightServer.render({
3897+
element: <Fookey="Outer Key"/>,
3898+
});
3899+
constmodel=awaitReactNoopFlightClient.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
functiongetVirtualPathFrame(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
exportconstREACT_MEMO_CACHE_SENTINEL: symbol=Symbol.for(
7373
'react.memo_cache_sentinel',
7474
);
75+
76+
importtype{ReactOptimisticKey}from'shared/ReactTypes';
77+
78+
exportconstREACT_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+
consterrors=[];
1118+
1119+
letresolve;
1120+
constpromise=newPromise(r=>(resolve=r));
1121+
1122+
asyncfunctionComponent(){
1123+
awaitpromise;
1124+
return'Hi';
1125+
}
1126+
1127+
if(React.optimisticKey===undefined){
1128+
thrownewError('optimisticKey missing');
1129+
}
1130+
1131+
functionApp(){
1132+
return(
1133+
<div>
1134+
<Suspensefallback="Loading">
1135+
<Componentkey={React.optimisticKey}/>
1136+
</Suspense>
1137+
</div>
1138+
);
1139+
}
1140+
1141+
constcontroller=newAbortController();
1142+
constpendingResult=serverAct(()=>
1143+
ReactDOMFizzStatic.prerender(<App/>,{
1144+
signal: controller.signal,
1145+
onError(x){
1146+
errors.push(x.message);
1147+
},
1148+
}),
1149+
);
1150+
1151+
awaitserverAct(()=>{
1152+
controller.abort();
1153+
});
1154+
1155+
constprerendered=awaitpendingResult;
1156+
1157+
constpostponedState=JSON.stringify(prerendered.postponed);
1158+
1159+
awaitreadIntoContainer(prerendered.prelude);
1160+
expect(getVisibleChildren(container)).toEqual(<div>Loading</div>);
1161+
1162+
expect(prerendered.postponed).not.toBe(null);
1163+
1164+
awaitresolve();
1165+
1166+
constdynamic=awaitserverAct(()=>
1167+
ReactDOMFizzServer.resume(<App/>,JSON.parse(postponedState)),
1168+
);
1169+
1170+
awaitreadIntoContainer(dynamic);
1171+
1172+
expect(getVisibleChildren(container)).toEqual(<div>Hi</div>);
1173+
});
11141174
});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Add expertimental `optimisticKey` behind a flag (#35162) · react/react@eb89912 · GitHub
Skip to content

Commit eb89912

Browse files
authored
Add expertimental optimisticKey behind a flag (#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

Lines changed: 454 additions & 83 deletions

‎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+
functionBar({text}){
3891+
return<div/>;
3892+
}
3893+
functionFoo(){
3894+
return<Barkey={ReactServer.optimisticKey}/>;
3895+
}
3896+
consttransport=ReactNoopFlightServer.render({
3897+
element: <Fookey="Outer Key"/>,
3898+
});
3899+
constmodel=awaitReactNoopFlightClient.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
functiongetVirtualPathFrame(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
exportconstREACT_MEMO_CACHE_SENTINEL: symbol=Symbol.for(
7373
'react.memo_cache_sentinel',
7474
);
75+
76+
importtype{ReactOptimisticKey}from'shared/ReactTypes';
77+
78+
exportconstREACT_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+
consterrors=[];
1118+
1119+
letresolve;
1120+
constpromise=newPromise(r=>(resolve=r));
1121+
1122+
asyncfunctionComponent(){
1123+
awaitpromise;
1124+
return'Hi';
1125+
}
1126+
1127+
if(React.optimisticKey===undefined){
1128+
thrownewError('optimisticKey missing');
1129+
}
1130+
1131+
functionApp(){
1132+
return(
1133+
<div>
1134+
<Suspensefallback="Loading">
1135+
<Componentkey={React.optimisticKey}/>
1136+
</Suspense>
1137+
</div>
1138+
);
1139+
}
1140+
1141+
constcontroller=newAbortController();
1142+
constpendingResult=serverAct(()=>
1143+
ReactDOMFizzStatic.prerender(<App/>,{
1144+
signal: controller.signal,
1145+
onError(x){
1146+
errors.push(x.message);
1147+
},
1148+
}),
1149+
);
1150+
1151+
awaitserverAct(()=>{
1152+
controller.abort();
1153+
});
1154+
1155+
constprerendered=awaitpendingResult;
1156+
1157+
constpostponedState=JSON.stringify(prerendered.postponed);
1158+
1159+
awaitreadIntoContainer(prerendered.prelude);
1160+
expect(getVisibleChildren(container)).toEqual(<div>Loading</div>);
1161+
1162+
expect(prerendered.postponed).not.toBe(null);
1163+
1164+
awaitresolve();
1165+
1166+
constdynamic=awaitserverAct(()=>
1167+
ReactDOMFizzServer.resume(<App/>,JSON.parse(postponedState)),
1168+
);
1169+
1170+
awaitreadIntoContainer(dynamic);
1171+
1172+
expect(getVisibleChildren(container)).toEqual(<div>Hi</div>);
1173+
});
11141174
});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add expertimental `optimisticKey` behind a flag (#35162) · react/react@eb89912 · GitHub
Skip to content

Commit eb89912

Browse files
authored
Add expertimental optimisticKey behind a flag (#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

Lines changed: 454 additions & 83 deletions

‎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+
functionBar({text}){
3891+
return<div/>;
3892+
}
3893+
functionFoo(){
3894+
return<Barkey={ReactServer.optimisticKey}/>;
3895+
}
3896+
consttransport=ReactNoopFlightServer.render({
3897+
element: <Fookey="Outer Key"/>,
3898+
});
3899+
constmodel=awaitReactNoopFlightClient.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
functiongetVirtualPathFrame(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
exportconstREACT_MEMO_CACHE_SENTINEL: symbol=Symbol.for(
7373
'react.memo_cache_sentinel',
7474
);
75+
76+
importtype{ReactOptimisticKey}from'shared/ReactTypes';
77+
78+
exportconstREACT_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+
consterrors=[];
1118+
1119+
letresolve;
1120+
constpromise=newPromise(r=>(resolve=r));
1121+
1122+
asyncfunctionComponent(){
1123+
awaitpromise;
1124+
return'Hi';
1125+
}
1126+
1127+
if(React.optimisticKey===undefined){
1128+
thrownewError('optimisticKey missing');
1129+
}
1130+
1131+
functionApp(){
1132+
return(
1133+
<div>
1134+
<Suspensefallback="Loading">
1135+
<Componentkey={React.optimisticKey}/>
1136+
</Suspense>
1137+
</div>
1138+
);
1139+
}
1140+
1141+
constcontroller=newAbortController();
1142+
constpendingResult=serverAct(()=>
1143+
ReactDOMFizzStatic.prerender(<App/>,{
1144+
signal: controller.signal,
1145+
onError(x){
1146+
errors.push(x.message);
1147+
},
1148+
}),
1149+
);
1150+
1151+
awaitserverAct(()=>{
1152+
controller.abort();
1153+
});
1154+
1155+
constprerendered=awaitpendingResult;
1156+
1157+
constpostponedState=JSON.stringify(prerendered.postponed);
1158+
1159+
awaitreadIntoContainer(prerendered.prelude);
1160+
expect(getVisibleChildren(container)).toEqual(<div>Loading</div>);
1161+
1162+
expect(prerendered.postponed).not.toBe(null);
1163+
1164+
awaitresolve();
1165+
1166+
constdynamic=awaitserverAct(()=>
1167+
ReactDOMFizzServer.resume(<App/>,JSON.parse(postponedState)),
1168+
);
1169+
1170+
awaitreadIntoContainer(dynamic);
1171+
1172+
expect(getVisibleChildren(container)).toEqual(<div>Hi</div>);
1173+
});
11141174
});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add expertimental `optimisticKey` behind a flag (#35162) · react/react@eb89912 · GitHub
Skip to content

Commit eb89912

Browse files
authored
Add expertimental optimisticKey behind a flag (#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

Lines changed: 454 additions & 83 deletions

‎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+
functionBar({text}){
3891+
return<div/>;
3892+
}
3893+
functionFoo(){
3894+
return<Barkey={ReactServer.optimisticKey}/>;
3895+
}
3896+
consttransport=ReactNoopFlightServer.render({
3897+
element: <Fookey="Outer Key"/>,
3898+
});
3899+
constmodel=awaitReactNoopFlightClient.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
functiongetVirtualPathFrame(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
exportconstREACT_MEMO_CACHE_SENTINEL: symbol=Symbol.for(
7373
'react.memo_cache_sentinel',
7474
);
75+
76+
importtype{ReactOptimisticKey}from'shared/ReactTypes';
77+
78+
exportconstREACT_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+
consterrors=[];
1118+
1119+
letresolve;
1120+
constpromise=newPromise(r=>(resolve=r));
1121+
1122+
asyncfunctionComponent(){
1123+
awaitpromise;
1124+
return'Hi';
1125+
}
1126+
1127+
if(React.optimisticKey===undefined){
1128+
thrownewError('optimisticKey missing');
1129+
}
1130+
1131+
functionApp(){
1132+
return(
1133+
<div>
1134+
<Suspensefallback="Loading">
1135+
<Componentkey={React.optimisticKey}/>
1136+
</Suspense>
1137+
</div>
1138+
);
1139+
}
1140+
1141+
constcontroller=newAbortController();
1142+
constpendingResult=serverAct(()=>
1143+
ReactDOMFizzStatic.prerender(<App/>,{
1144+
signal: controller.signal,
1145+
onError(x){
1146+
errors.push(x.message);
1147+
},
1148+
}),
1149+
);
1150+
1151+
awaitserverAct(()=>{
1152+
controller.abort();
1153+
});
1154+
1155+
constprerendered=awaitpendingResult;
1156+
1157+
constpostponedState=JSON.stringify(prerendered.postponed);
1158+
1159+
awaitreadIntoContainer(prerendered.prelude);
1160+
expect(getVisibleChildren(container)).toEqual(<div>Loading</div>);
1161+
1162+
expect(prerendered.postponed).not.toBe(null);
1163+
1164+
awaitresolve();
1165+
1166+
constdynamic=awaitserverAct(()=>
1167+
ReactDOMFizzServer.resume(<App/>,JSON.parse(postponedState)),
1168+
);
1169+
1170+
awaitreadIntoContainer(dynamic);
1171+
1172+
expect(getVisibleChildren(container)).toEqual(<div>Hi</div>);
1173+
});
11141174
});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Add expertimental `optimisticKey` behind a flag (#35162) · react/react@eb89912 · GitHub
Skip to content

Commit eb89912

Browse files
authored
Add expertimental optimisticKey behind a flag (#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

Lines changed: 454 additions & 83 deletions

‎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+
functionBar({text}){
3891+
return<div/>;
3892+
}
3893+
functionFoo(){
3894+
return<Barkey={ReactServer.optimisticKey}/>;
3895+
}
3896+
consttransport=ReactNoopFlightServer.render({
3897+
element: <Fookey="Outer Key"/>,
3898+
});
3899+
constmodel=awaitReactNoopFlightClient.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
functiongetVirtualPathFrame(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
exportconstREACT_MEMO_CACHE_SENTINEL: symbol=Symbol.for(
7373
'react.memo_cache_sentinel',
7474
);
75+
76+
importtype{ReactOptimisticKey}from'shared/ReactTypes';
77+
78+
exportconstREACT_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+
consterrors=[];
1118+
1119+
letresolve;
1120+
constpromise=newPromise(r=>(resolve=r));
1121+
1122+
asyncfunctionComponent(){
1123+
awaitpromise;
1124+
return'Hi';
1125+
}
1126+
1127+
if(React.optimisticKey===undefined){
1128+
thrownewError('optimisticKey missing');
1129+
}
1130+
1131+
functionApp(){
1132+
return(
1133+
<div>
1134+
<Suspensefallback="Loading">
1135+
<Componentkey={React.optimisticKey}/>
1136+
</Suspense>
1137+
</div>
1138+
);
1139+
}
1140+
1141+
constcontroller=newAbortController();
1142+
constpendingResult=serverAct(()=>
1143+
ReactDOMFizzStatic.prerender(<App/>,{
1144+
signal: controller.signal,
1145+
onError(x){
1146+
errors.push(x.message);
1147+
},
1148+
}),
1149+
);
1150+
1151+
awaitserverAct(()=>{
1152+
controller.abort();
1153+
});
1154+
1155+
constprerendered=awaitpendingResult;
1156+
1157+
constpostponedState=JSON.stringify(prerendered.postponed);
1158+
1159+
awaitreadIntoContainer(prerendered.prelude);
1160+
expect(getVisibleChildren(container)).toEqual(<div>Loading</div>);
1161+
1162+
expect(prerendered.postponed).not.toBe(null);
1163+
1164+
awaitresolve();
1165+
1166+
constdynamic=awaitserverAct(()=>
1167+
ReactDOMFizzServer.resume(<App/>,JSON.parse(postponedState)),
1168+
);
1169+
1170+
awaitreadIntoContainer(dynamic);
1171+
1172+
expect(getVisibleChildren(container)).toEqual(<div>Hi</div>);
1173+
});
11141174
});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add expertimental `optimisticKey` behind a flag (#35162) · react/react@eb89912 · GitHub
Skip to content

Commit eb89912

Browse files
authored
Add expertimental optimisticKey behind a flag (#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

Lines changed: 454 additions & 83 deletions

‎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+
functionBar({text}){
3891+
return<div/>;
3892+
}
3893+
functionFoo(){
3894+
return<Barkey={ReactServer.optimisticKey}/>;
3895+
}
3896+
consttransport=ReactNoopFlightServer.render({
3897+
element: <Fookey="Outer Key"/>,
3898+
});
3899+
constmodel=awaitReactNoopFlightClient.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
functiongetVirtualPathFrame(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
exportconstREACT_MEMO_CACHE_SENTINEL: symbol=Symbol.for(
7373
'react.memo_cache_sentinel',
7474
);
75+
76+
importtype{ReactOptimisticKey}from'shared/ReactTypes';
77+
78+
exportconstREACT_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+
consterrors=[];
1118+
1119+
letresolve;
1120+
constpromise=newPromise(r=>(resolve=r));
1121+
1122+
asyncfunctionComponent(){
1123+
awaitpromise;
1124+
return'Hi';
1125+
}
1126+
1127+
if(React.optimisticKey===undefined){
1128+
thrownewError('optimisticKey missing');
1129+
}
1130+
1131+
functionApp(){
1132+
return(
1133+
<div>
1134+
<Suspensefallback="Loading">
1135+
<Componentkey={React.optimisticKey}/>
1136+
</Suspense>
1137+
</div>
1138+
);
1139+
}
1140+
1141+
constcontroller=newAbortController();
1142+
constpendingResult=serverAct(()=>
1143+
ReactDOMFizzStatic.prerender(<App/>,{
1144+
signal: controller.signal,
1145+
onError(x){
1146+
errors.push(x.message);
1147+
},
1148+
}),
1149+
);
1150+
1151+
awaitserverAct(()=>{
1152+
controller.abort();
1153+
});
1154+
1155+
constprerendered=awaitpendingResult;
1156+
1157+
constpostponedState=JSON.stringify(prerendered.postponed);
1158+
1159+
awaitreadIntoContainer(prerendered.prelude);
1160+
expect(getVisibleChildren(container)).toEqual(<div>Loading</div>);
1161+
1162+
expect(prerendered.postponed).not.toBe(null);
1163+
1164+
awaitresolve();
1165+
1166+
constdynamic=awaitserverAct(()=>
1167+
ReactDOMFizzServer.resume(<App/>,JSON.parse(postponedState)),
1168+
);
1169+
1170+
awaitreadIntoContainer(dynamic);
1171+
1172+
expect(getVisibleChildren(container)).toEqual(<div>Hi</div>);
1173+
});
11141174
});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add expertimental `optimisticKey` behind a flag (#35162) · react/react@eb89912 · GitHub
Skip to content

Commit eb89912

Browse files
authored
Add expertimental optimisticKey behind a flag (#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

Lines changed: 454 additions & 83 deletions

‎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+
functionBar({text}){
3891+
return<div/>;
3892+
}
3893+
functionFoo(){
3894+
return<Barkey={ReactServer.optimisticKey}/>;
3895+
}
3896+
consttransport=ReactNoopFlightServer.render({
3897+
element: <Fookey="Outer Key"/>,
3898+
});
3899+
constmodel=awaitReactNoopFlightClient.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
functiongetVirtualPathFrame(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
exportconstREACT_MEMO_CACHE_SENTINEL: symbol=Symbol.for(
7373
'react.memo_cache_sentinel',
7474
);
75+
76+
importtype{ReactOptimisticKey}from'shared/ReactTypes';
77+
78+
exportconstREACT_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+
consterrors=[];
1118+
1119+
letresolve;
1120+
constpromise=newPromise(r=>(resolve=r));
1121+
1122+
asyncfunctionComponent(){
1123+
awaitpromise;
1124+
return'Hi';
1125+
}
1126+
1127+
if(React.optimisticKey===undefined){
1128+
thrownewError('optimisticKey missing');
1129+
}
1130+
1131+
functionApp(){
1132+
return(
1133+
<div>
1134+
<Suspensefallback="Loading">
1135+
<Componentkey={React.optimisticKey}/>
1136+
</Suspense>
1137+
</div>
1138+
);
1139+
}
1140+
1141+
constcontroller=newAbortController();
1142+
constpendingResult=serverAct(()=>
1143+
ReactDOMFizzStatic.prerender(<App/>,{
1144+
signal: controller.signal,
1145+
onError(x){
1146+
errors.push(x.message);
1147+
},
1148+
}),
1149+
);
1150+
1151+
awaitserverAct(()=>{
1152+
controller.abort();
1153+
});
1154+
1155+
constprerendered=awaitpendingResult;
1156+
1157+
constpostponedState=JSON.stringify(prerendered.postponed);
1158+
1159+
awaitreadIntoContainer(prerendered.prelude);
1160+
expect(getVisibleChildren(container)).toEqual(<div>Loading</div>);
1161+
1162+
expect(prerendered.postponed).not.toBe(null);
1163+
1164+
awaitresolve();
1165+
1166+
constdynamic=awaitserverAct(()=>
1167+
ReactDOMFizzServer.resume(<App/>,JSON.parse(postponedState)),
1168+
);
1169+
1170+
awaitreadIntoContainer(dynamic);
1171+
1172+
expect(getVisibleChildren(container)).toEqual(<div>Hi</div>);
1173+
});
11141174
});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Add expertimental `optimisticKey` behind a flag (#35162) · react/react@eb89912 · GitHub
Skip to content

Commit eb89912

Browse files
authored
Add expertimental optimisticKey behind a flag (#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

Lines changed: 454 additions & 83 deletions

‎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+
functionBar({text}){
3891+
return<div/>;
3892+
}
3893+
functionFoo(){
3894+
return<Barkey={ReactServer.optimisticKey}/>;
3895+
}
3896+
consttransport=ReactNoopFlightServer.render({
3897+
element: <Fookey="Outer Key"/>,
3898+
});
3899+
constmodel=awaitReactNoopFlightClient.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
functiongetVirtualPathFrame(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
exportconstREACT_MEMO_CACHE_SENTINEL: symbol=Symbol.for(
7373
'react.memo_cache_sentinel',
7474
);
75+
76+
importtype{ReactOptimisticKey}from'shared/ReactTypes';
77+
78+
exportconstREACT_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+
consterrors=[];
1118+
1119+
letresolve;
1120+
constpromise=newPromise(r=>(resolve=r));
1121+
1122+
asyncfunctionComponent(){
1123+
awaitpromise;
1124+
return'Hi';
1125+
}
1126+
1127+
if(React.optimisticKey===undefined){
1128+
thrownewError('optimisticKey missing');
1129+
}
1130+
1131+
functionApp(){
1132+
return(
1133+
<div>
1134+
<Suspensefallback="Loading">
1135+
<Componentkey={React.optimisticKey}/>
1136+
</Suspense>
1137+
</div>
1138+
);
1139+
}
1140+
1141+
constcontroller=newAbortController();
1142+
constpendingResult=serverAct(()=>
1143+
ReactDOMFizzStatic.prerender(<App/>,{
1144+
signal: controller.signal,
1145+
onError(x){
1146+
errors.push(x.message);
1147+
},
1148+
}),
1149+
);
1150+
1151+
awaitserverAct(()=>{
1152+
controller.abort();
1153+
});
1154+
1155+
constprerendered=awaitpendingResult;
1156+
1157+
constpostponedState=JSON.stringify(prerendered.postponed);
1158+
1159+
awaitreadIntoContainer(prerendered.prelude);
1160+
expect(getVisibleChildren(container)).toEqual(<div>Loading</div>);
1161+
1162+
expect(prerendered.postponed).not.toBe(null);
1163+
1164+
awaitresolve();
1165+
1166+
constdynamic=awaitserverAct(()=>
1167+
ReactDOMFizzServer.resume(<App/>,JSON.parse(postponedState)),
1168+
);
1169+
1170+
awaitreadIntoContainer(dynamic);
1171+
1172+
expect(getVisibleChildren(container)).toEqual(<div>Hi</div>);
1173+
});
11141174
});

0 commit comments

Comments
 (0)