@samitouri / QOS-React-2 / commits / 3278d24218

Add useOpaqueIdentifier Hook (#17322)

* Add useOpaqueIdentifier Hook We currently use unique IDs in a lot of places. Examples are: * `<label for="ID">` * `aria-labelledby` This can cause some issues: 1. If we server side render and then hydrate, this could cause an hydration ID mismatch 2. If we server side render one part of the page and client side render another part of the page, the ID for one part could be different than the ID for another part even though they are supposed to be the same 3. If we conditionally render something with an ID , this might also cause an ID mismatch because the ID will be different on other parts of the page This PR creates a new hook `useUniqueId` that generates a different unique ID based on whether the hook was called on the server or client. If the hook is called during hydration, it generates an opaque object that will rerender the hook so that the IDs match. Co-authored-by: Andrew Clark <git@andrewclark.io>

Luna Ruan committed Apr 6, 2020 at 17:17 UTC 3278d242184a13add3f25f683b77ef9a6a2305f3
24 files changed +1370 -9
packages/react-art/src/ReactARTHostConfig.js
+22
@@ -470,6 +470,28 @@ export function beforeRemoveInstance(instance) {
470 // noop
471 }
472
473 +export function isOpaqueHydratingObject(value: mixed): boolean {
474 + throw new Error('Not yet implemented');
475 +}
476 +
477 +export function makeOpaqueHydratingObject(
478 + attemptToReadValue: () => void,
479 +): OpaqueIDType {
480 + throw new Error('Not yet implemented.');
481 +}
482 +
483 +export function makeClientId(): OpaqueIDType {
484 + throw new Error('Not yet implemented');
485 +}
486 +
487 +export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
488 + throw new Error('Not yet implemented');
489 +}
490 +
491 +export function makeServerId(): OpaqueIDType {
492 + throw new Error('Not yet implemented');
493 +}
494 +
495 export function registerEvent(event: any, rootContainerInstance: any) {
496 throw new Error('Not yet implemented.');
497 }
packages/react-debug-tools/src/ReactDebugHooks.js
+26
@@ -18,12 +18,16 @@ import type {
18 ReactScopeMethods,
19 } from 'shared/ReactTypes';
20 import type {Fiber} from 'react-reconciler/src/ReactFiber';
21 +import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig';
22 +
23 import type {Hook, TimeoutConfig} from 'react-reconciler/src/ReactFiberHooks';
24 import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactFiberHooks';
25 import type {SuspenseConfig} from 'react-reconciler/src/ReactFiberSuspenseConfig';
26 +import {NoMode} from 'react-reconciler/src/ReactTypeOfMode';
27
28 import ErrorStackParser from 'error-stack-parser';
29 import ReactSharedInternals from 'shared/ReactSharedInternals';
30 +import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols';
31 import {
32 FunctionComponent,
33 SimpleMemoComponent,
@@ -61,6 +65,8 @@ type Dispatch<A> = A => void;
65
66 let primitiveStackCache: null | Map<string, Array<any>> = null;
67
68 +let currentFiber: Fiber | null = null;
69 +
70 function getPrimitiveStackCache(): Map<string, Array<any>> {
71 // This initializes a cache of all primitive hooks so that the top
72 // most stack frames added by calling the primitive hook can be removed.
@@ -319,6 +325,23 @@ function useDeferredValue<T>(value: T, config: TimeoutConfig | null | void): T {
325 return value;
326 }
327
328 +function useOpaqueIdentifier(): OpaqueIDType | void {
329 + const hook = nextHook(); // State
330 + if (currentFiber && currentFiber.mode === NoMode) {
331 + nextHook(); // Effect
332 + }
333 + let value = hook === null ? undefined : hook.memoizedState;
334 + if (value && value.$$typeof === REACT_OPAQUE_ID_TYPE) {
335 + value = undefined;
336 + }
337 + hookLog.push({
338 + primitive: 'OpaqueIdentifier',
339 + stackError: new Error(),
340 + value,
341 + });
342 + return value;
343 +}
344 +
345 const Dispatcher: DispatcherType = {
346 readContext,
347 useCallback,
@@ -336,6 +359,7 @@ const Dispatcher: DispatcherType = {
359 useMutableSource,
360 useDeferredValue,
361 useEvent,
362 + useOpaqueIdentifier,
363 };
364
365 // Inspect
@@ -684,6 +708,8 @@ export function inspectHooksOfFiber(
708 currentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
709 }
710
711 + currentFiber = fiber;
712 +
713 if (
714 fiber.tag !== FunctionComponent &&
715 fiber.tag !== SimpleMemoComponent &&
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+58
@@ -422,6 +422,64 @@ describe('ReactHooksInspectionIntegration', () => {
422 },
423 ]);
424 });
425 +
426 + it('should support composite useOpaqueIdentifier hook', () => {
427 + function Foo(props) {
428 + const id = React.unstable_useOpaqueIdentifier();
429 + const [state] = React.useState(() => 'hello', []);
430 + return <div id={id}>{state}</div>;
431 + }
432 +
433 + const renderer = ReactTestRenderer.create(<Foo />);
434 + const childFiber = renderer.root.findByType(Foo)._currentFiber();
435 + const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
436 +
437 + expect(tree.length).toEqual(2);
438 +
439 + expect(tree[0].id).toEqual(0);
440 + expect(tree[0].isStateEditable).toEqual(false);
441 + expect(tree[0].name).toEqual('OpaqueIdentifier');
442 + expect((tree[0].value + '').startsWith('c_')).toBe(true);
443 +
444 + expect(tree[1]).toEqual({
445 + id: 1,
446 + isStateEditable: true,
447 + name: 'State',
448 + value: 'hello',
449 + subHooks: [],
450 + });
451 + });
452 +
453 + it('should support composite useOpaqueIdentifier hook in concurrent mode', () => {
454 + function Foo(props) {
455 + const id = React.unstable_useOpaqueIdentifier();
456 + const [state] = React.useState(() => 'hello', []);
457 + return <div id={id}>{state}</div>;
458 + }
459 +
460 + const renderer = ReactTestRenderer.create(<Foo />, {
461 + unstable_isConcurrent: true,
462 + });
463 + expect(Scheduler).toFlushWithoutYielding();
464 +
465 + const childFiber = renderer.root.findByType(Foo)._currentFiber();
466 + const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
467 +
468 + expect(tree.length).toEqual(2);
469 +
470 + expect(tree[0].id).toEqual(0);
471 + expect(tree[0].isStateEditable).toEqual(false);
472 + expect(tree[0].name).toEqual('OpaqueIdentifier');
473 + expect((tree[0].value + '').startsWith('c_')).toBe(true);
474 +
475 + expect(tree[1]).toEqual({
476 + id: 1,
477 + isStateEditable: true,
478 + name: 'State',
479 + value: 'hello',
480 + subHooks: [],
481 + });
482 + });
483 }
484
485 describe('useDebugValue', () => {
packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js
+906
@@ -18,6 +18,7 @@ let ReactFeatureFlags;
18 let ReactDOM;
19 let ReactDOMServer;
20 let ReactTestUtils;
21 +let Scheduler;
22 let useState;
23 let useReducer;
24 let useEffect;
@@ -28,6 +29,7 @@ let useRef;
29 let useImperativeHandle;
30 let useLayoutEffect;
31 let useDebugValue;
32 +let useOpaqueIdentifier;
33 let forwardRef;
34 let yieldedValues;
35 let yieldValue;
@@ -39,10 +41,12 @@ function initModules() {
41
42 ReactFeatureFlags = require('shared/ReactFeatureFlags');
43 ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
44 + ReactFeatureFlags.flushSuspenseFallbacksInTests = false;
45 React = require('react');
46 ReactDOM = require('react-dom');
47 ReactDOMServer = require('react-dom/server');
48 ReactTestUtils = require('react-dom/test-utils');
49 + Scheduler = require('scheduler');
50 useState = React.useState;
51 useReducer = React.useReducer;
52 useEffect = React.useEffect;
@@ -53,6 +57,7 @@ function initModules() {
57 useDebugValue = React.useDebugValue;
58 useImperativeHandle = React.useImperativeHandle;
59 useLayoutEffect = React.useLayoutEffect;
60 + useOpaqueIdentifier = React.unstable_useOpaqueIdentifier;
61 forwardRef = React.forwardRef;
62
63 yieldedValues = [];
@@ -78,6 +83,9 @@ const {
83 itRenders,
84 itThrowsWhenRendering,
85 serverRender,
86 + streamRender,
87 + clientCleanRender,
88 + clientRenderOnServerString,
89 } = ReactDOMServerIntegrationUtils(initModules);
90
91 describe('ReactDOMServerHooks', () => {
@@ -834,4 +842,902 @@ describe('ReactDOMServerHooks', () => {
842 expect(domNode2.textContent).toEqual('42');
843 });
844 });
845 +
846 + if (__EXPERIMENTAL__) {
847 + describe('useOpaqueIdentifier', () => {
848 + it('generates unique ids for server string render', async () => {
849 + function App(props) {
850 + const idOne = useOpaqueIdentifier();
851 + const idTwo = useOpaqueIdentifier();
852 + return (
853 + <div>
854 + <div aria-labelledby={idOne} />
855 + <div id={idOne} />
856 + <span aria-labelledby={idTwo} />
857 + <span id={idTwo} />
858 + </div>
859 + );
860 + }
861 +
862 + const domNode = await serverRender(<App />);
863 + expect(domNode.children.length).toEqual(4);
864 + expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual(
865 + domNode.children[1].getAttribute('id'),
866 + );
867 + expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual(
868 + domNode.children[3].getAttribute('id'),
869 + );
870 + expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual(
871 + domNode.children[2].getAttribute('aria-labelledby'),
872 + );
873 + expect(
874 + domNode.children[0].getAttribute('aria-labelledby'),
875 + ).not.toBeNull();
876 + expect(
877 + domNode.children[2].getAttribute('aria-labelledby'),
878 + ).not.toBeNull();
879 + });
880 +
881 + it('generates unique ids for server stream render', async () => {
882 + function App(props) {
883 + const idOne = useOpaqueIdentifier();
884 + const idTwo = useOpaqueIdentifier();
885 + return (
886 + <div>
887 + <div aria-labelledby={idOne} />
888 + <div id={idOne} />
889 + <span aria-labelledby={idTwo} />
890 + <span id={idTwo} />
891 + </div>
892 + );
893 + }
894 +
895 + const domNode = await streamRender(<App />);
896 + expect(domNode.children.length).toEqual(4);
897 + expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual(
898 + domNode.children[1].getAttribute('id'),
899 + );
900 + expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual(
901 + domNode.children[3].getAttribute('id'),
902 + );
903 + expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual(
904 + domNode.children[2].getAttribute('aria-labelledby'),
905 + );
906 + expect(
907 + domNode.children[0].getAttribute('aria-labelledby'),
908 + ).not.toBeNull();
909 + expect(
910 + domNode.children[2].getAttribute('aria-labelledby'),
911 + ).not.toBeNull();
912 + });
913 +
914 + it('generates unique ids for client render', async () => {
915 + function App(props) {
916 + const idOne = useOpaqueIdentifier();
917 + const idTwo = useOpaqueIdentifier();
918 + return (
919 + <div>
920 + <div aria-labelledby={idOne} />
921 + <div id={idOne} />
922 + <span aria-labelledby={idTwo} />
923 + <span id={idTwo} />
924 + </div>
925 + );
926 + }
927 +
928 + const domNode = await clientCleanRender(<App />);
929 + expect(domNode.children.length).toEqual(4);
930 + expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual(
931 + domNode.children[1].getAttribute('id'),
932 + );
933 + expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual(
934 + domNode.children[3].getAttribute('id'),
935 + );
936 + expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual(
937 + domNode.children[2].getAttribute('aria-labelledby'),
938 + );
939 + expect(
940 + domNode.children[0].getAttribute('aria-labelledby'),
941 + ).not.toBeNull();
942 + expect(
943 + domNode.children[2].getAttribute('aria-labelledby'),
944 + ).not.toBeNull();
945 + });
946 +
947 + it('generates unique ids for client render on good server markup', async () => {
948 + function App(props) {
949 + const idOne = useOpaqueIdentifier();
950 + const idTwo = useOpaqueIdentifier();
951 + return (
952 + <div>
953 + <div aria-labelledby={idOne} />
954 + <div id={idOne} />
955 + <span aria-labelledby={idTwo} />
956 + <span id={idTwo} />
957 + </div>
958 + );
959 + }
960 +
961 + const domNode = await clientRenderOnServerString(<App />);
962 + expect(domNode.children.length).toEqual(4);
963 + expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual(
964 + domNode.children[1].getAttribute('id'),
965 + );
966 + expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual(
967 + domNode.children[3].getAttribute('id'),
968 + );
969 + expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual(
970 + domNode.children[2].getAttribute('aria-labelledby'),
971 + );
972 + expect(
973 + domNode.children[0].getAttribute('aria-labelledby'),
974 + ).not.toBeNull();
975 + expect(
976 + domNode.children[2].getAttribute('aria-labelledby'),
977 + ).not.toBeNull();
978 + });
979 +
980 + it('useOpaqueIdentifier does not change id even if the component updates during client render', async () => {
981 + let _setShowId;
982 + function App() {
983 + const id = useOpaqueIdentifier();
984 + const [showId, setShowId] = useState(false);
985 + _setShowId = setShowId;
986 + return (
987 + <div>
988 + <div aria-labelledby={id} />
989 + {showId && <div id={id} />}
990 + </div>
991 + );
992 + }
993 +
994 + const domNode = await clientCleanRender(<App />);
995 + const oldClientId = domNode.children[0].getAttribute('aria-labelledby');
996 +
997 + expect(domNode.children.length).toEqual(1);
998 + expect(oldClientId).not.toBeNull();
999 +
1000 + await ReactTestUtils.act(async () => _setShowId(true));
1001 +
1002 + expect(domNode.children.length).toEqual(2);
1003 + expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual(
1004 + domNode.children[1].getAttribute('id'),
1005 + );
1006 + expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual(
1007 + oldClientId,
1008 + );
1009 + });
1010 +
1011 + it('useOpaqueIdentifier: IDs match when, after hydration, a new component that uses the ID is rendered', async () => {
1012 + let _setShowDiv;
1013 + function App() {
1014 + const id = useOpaqueIdentifier();
1015 + const [showDiv, setShowDiv] = useState(false);
1016 + _setShowDiv = setShowDiv;
1017 +
1018 + return (
1019 + <div>
1020 + <div id={id}>Child One</div>
1021 + {showDiv && <div id={id}>Child Two</div>}
1022 + </div>
1023 + );
1024 + }
1025 +
1026 + const container = document.createElement('div');
1027 + document.body.append(container);
1028 +
1029 + container.innerHTML = ReactDOMServer.renderToString(<App />);
1030 + const root = ReactDOM.createRoot(container, {hydrate: true});
1031 + root.render(<App />);
1032 + Scheduler.unstable_flushAll();
1033 + jest.runAllTimers();
1034 +
1035 + expect(container.children[0].children.length).toEqual(1);
1036 + const oldServerId = container.children[0].children[0].getAttribute(
1037 + 'id',
1038 + );
1039 + expect(oldServerId).not.toBeNull();
1040 +
1041 + await ReactTestUtils.act(async () => {
1042 + _setShowDiv(true);
1043 + });
1044 + expect(container.children[0].children.length).toEqual(2);
1045 + expect(container.children[0].children[0].getAttribute('id')).toEqual(
1046 + container.children[0].children[1].getAttribute('id'),
1047 + );
1048 + expect(
1049 + container.children[0].children[0].getAttribute('id'),
1050 + ).not.toEqual(oldServerId);
1051 + expect(
1052 + container.children[0].children[0].getAttribute('id'),
1053 + ).not.toBeNull();
1054 + });
1055 +
1056 + it('useOpaqueIdentifier: IDs match when, after hydration, a new component that uses the ID is rendered for legacy', async () => {
1057 + let _setShowDiv;
1058 + function App() {
1059 + const id = useOpaqueIdentifier();
1060 + const [showDiv, setShowDiv] = useState(false);
1061 + _setShowDiv = setShowDiv;
1062 +
1063 + return (
1064 + <div>
1065 + <div id={id}>Child One</div>
1066 + {showDiv && <div id={id}>Child Two</div>}
1067 + </div>
1068 + );
1069 + }
1070 +
1071 + const container = document.createElement('div');
1072 + document.body.append(container);
1073 +
1074 + container.innerHTML = ReactDOMServer.renderToString(<App />);
1075 + ReactDOM.hydrate(<App />, container);
1076 +
1077 + expect(container.children[0].children.length).toEqual(1);
1078 + const oldServerId = container.children[0].children[0].getAttribute(
1079 + 'id',
1080 + );
1081 + expect(oldServerId).not.toBeNull();
1082 +
1083 + await ReactTestUtils.act(async () => {
1084 + _setShowDiv(true);
1085 + });
1086 + expect(container.children[0].children.length).toEqual(2);
1087 + expect(container.children[0].children[0].getAttribute('id')).toEqual(
1088 + container.children[0].children[1].getAttribute('id'),
1089 + );
1090 + expect(
1091 + container.children[0].children[0].getAttribute('id'),
1092 + ).not.toEqual(oldServerId);
1093 + expect(
1094 + container.children[0].children[0].getAttribute('id'),
1095 + ).not.toBeNull();
1096 + });
1097 +
1098 + it('useOpaqueIdentifier: ID is not used during hydration but is used in an update', async () => {
1099 + let _setShow;
1100 + function App() {
1101 + Scheduler.unstable_yieldValue('App');
1102 + const id = useOpaqueIdentifier();
1103 + const [show, setShow] = useState(false);
1104 + _setShow = setShow;
1105 + return (
1106 + <div>
1107 + <span id={show ? id : null}>{'Child One'}</span>
1108 + </div>
1109 + );
1110 + }
1111 +
1112 + const container = document.createElement('div');
1113 + document.body.append(container);
1114 + container.innerHTML = ReactDOMServer.renderToString(<App />);
1115 + const root = ReactDOM.createRoot(container, {hydrate: true});
1116 + ReactTestUtils.act(() => {
1117 + root.render(<App />);
1118 + });
1119 + expect(Scheduler).toHaveYielded(['App', 'App']);
1120 + // The ID goes from not being used to being added to the page
1121 + ReactTestUtils.act(() => {
1122 + _setShow(true);
1123 + });
1124 + expect(Scheduler).toHaveYielded(['App', 'App']);
1125 + expect(
1126 + container.getElementsByTagName('span')[0].getAttribute('id'),
1127 + ).not.toBeNull();
1128 + });
1129 +
1130 + it('useOpaqueIdentifier: ID is not used during hydration but is used in an update in legacy', async () => {
1131 + let _setShow;
1132 + function App() {
1133 + Scheduler.unstable_yieldValue('App');
1134 + const id = useOpaqueIdentifier();
1135 + const [show, setShow] = useState(false);
1136 + _setShow = setShow;
1137 + return (
1138 + <div>
1139 + <span id={show ? id : null}>{'Child One'}</span>
1140 + </div>
1141 + );
1142 + }
1143 +
1144 + const container = document.createElement('div');
1145 + document.body.append(container);
1146 + container.innerHTML = ReactDOMServer.renderToString(<App />);
1147 + ReactDOM.hydrate(<App />, container);
1148 + expect(Scheduler).toHaveYielded(['App', 'App']);
1149 + // The ID goes from not being used to being added to the page
1150 + ReactTestUtils.act(() => {
1151 + _setShow(true);
1152 + });
1153 + expect(Scheduler).toHaveYielded(['App']);
1154 + expect(
1155 + container.getElementsByTagName('span')[0].getAttribute('id'),
1156 + ).not.toBeNull();
1157 + });
1158 +
1159 + it('useOpaqueIdentifierr: flushSync', async () => {
1160 + let _setShow;
1161 + function App() {
1162 + const id = useOpaqueIdentifier();
1163 + const [show, setShow] = useState(false);
1164 + _setShow = setShow;
1165 + return (
1166 + <div>
1167 + <span id={show ? id : null}>{'Child One'}</span>
1168 + </div>
1169 + );
1170 + }
1171 +
1172 + const container = document.createElement('div');
1173 + document.body.append(container);
1174 + container.innerHTML = ReactDOMServer.renderToString(<App />);
1175 + const root = ReactDOM.createRoot(container, {hydrate: true});
1176 + ReactTestUtils.act(() => {
1177 + root.render(<App />);
1178 + });
1179 +
1180 + // The ID goes from not being used to being added to the page
1181 + ReactTestUtils.act(() => {
1182 + ReactDOM.flushSync(() => {
1183 + _setShow(true);
1184 + });
1185 + });
1186 + expect(
1187 + container.getElementsByTagName('span')[0].getAttribute('id'),
1188 + ).not.toBeNull();
1189 + });
1190 +
1191 + it('useOpaqueIdentifier: children with id hydrates before other children if ID updates', async () => {
1192 + let _setShow;
1193 +
1194 + const child1Ref = React.createRef();
1195 + const childWithIDRef = React.createRef();
1196 + const setShowRef = React.createRef();
1197 +
1198 + // RENAME THESE
1199 + function Child1() {
1200 + Scheduler.unstable_yieldValue('Child One');
1201 + return <span ref={child1Ref}>{'Child One'}</span>;
1202 + }
1203 +
1204 + function Child2() {
1205 + Scheduler.unstable_yieldValue('Child Two');
1206 + return <span>{'Child Two'}</span>;
1207 + }
1208 +
1209 + const Children = React.memo(function Children() {
1210 + return (
1211 + <React.Suspense fallback="Loading 1...">
1212 + <Child1 />
1213 + <Child2 />
1214 + </React.Suspense>
1215 + );
1216 + });
1217 +
1218 + function ChildWithID({parentID}) {
1219 + Scheduler.unstable_yieldValue('Child with ID');
1220 + return (
1221 + <span id={parentID} ref={childWithIDRef}>
1222 + {'Child with ID'}
1223 + </span>
1224 + );
1225 + }
1226 +
1227 + const ChildrenWithID = React.memo(function ChildrenWithID({parentID}) {
1228 + return (
1229 + <React.Suspense fallback="Loading 2...">
1230 + <ChildWithID parentID={parentID} />
1231 + </React.Suspense>
1232 + );
1233 + });
1234 +
1235 + function App() {
1236 + const id = useOpaqueIdentifier();
1237 + const [show, setShow] = useState(false);
1238 + _setShow = setShow;
1239 + return (
1240 + <div>
1241 + <Children />
1242 + <ChildrenWithID parentID={id} />
1243 + {show && (
1244 + <span aria-labelledby={id} ref={setShowRef}>
1245 + {'Child Three'}
1246 + </span>
1247 + )}
1248 + </div>
1249 + );
1250 + }
1251 +
1252 + const container = document.createElement('div');
1253 + container.innerHTML = ReactDOMServer.renderToString(<App />);
1254 + expect(Scheduler).toHaveYielded([
1255 + 'Child One',
1256 + 'Child Two',
1257 + 'Child with ID',
1258 + ]);
1259 + expect(container.textContent).toEqual(
1260 + 'Child OneChild TwoChild with ID',
1261 + );
1262 +
1263 + const serverId = container
1264 + .getElementsByTagName('span')[2]
1265 + .getAttribute('id');
1266 + expect(serverId).not.toBeNull();
1267 +
1268 + const childOneSpan = container.getElementsByTagName('span')[0];
1269 +
1270 + const root = ReactDOM.createRoot(container, {hydrate: true});
1271 + root.render(<App show={false} />);
1272 + expect(Scheduler).toHaveYielded([]);
1273 +
1274 + //Hydrate just child one before updating state
1275 + expect(Scheduler).toFlushAndYieldThrough(['Child One']);
1276 + expect(child1Ref.current).toBe(null);
1277 + expect(Scheduler).toHaveYielded([]);
1278 +
1279 + ReactTestUtils.act(() => {
1280 + _setShow(true);
1281 +
1282 + // State update should trigger the ID to update, which changes the props
1283 + // of ChildWithID. This should cause ChildWithID to hydrate before Children
1284 + expect(Scheduler).toFlushAndYieldThrough([
1285 + 'Child with ID',
1286 + 'Child with ID',
1287 + 'Child with ID',
1288 + 'Child One',
1289 + 'Child Two',
1290 + ]);
1291 +
1292 + expect(child1Ref.current).toBe(null);
1293 + expect(childWithIDRef.current).toEqual(
1294 + container.getElementsByTagName('span')[2],
1295 + );
1296 +
1297 + expect(setShowRef.current).toEqual(
1298 + container.getElementsByTagName('span')[3],
1299 + );
1300 +
1301 + expect(childWithIDRef.current.getAttribute('id')).toEqual(
1302 + setShowRef.current.getAttribute('aria-labelledby'),
1303 + );
1304 + expect(childWithIDRef.current.getAttribute('id')).not.toEqual(
1305 + serverId,
1306 + );
1307 + });
1308 +
1309 + // Children hydrates after ChildWithID
1310 + expect(child1Ref.current).toBe(childOneSpan);
1311 +
1312 + Scheduler.unstable_flushAll();
1313 +
1314 + expect(Scheduler).toHaveYielded([]);
1315 + });
1316 +
1317 + it('useOpaqueIdentifier: IDs match when part of the DOM tree is server rendered and part is client rendered', async () => {
1318 + let suspend = true;
1319 + let resolve;
1320 + const promise = new Promise(
1321 + resolvePromise => (resolve = resolvePromise),
1322 + );
1323 +
1324 + function Child({text}) {
1325 + if (suspend) {
1326 + throw promise;
1327 + } else {
1328 + return text;
1329 + }
1330 + }
1331 +
1332 + function RenderedChild() {
1333 + useEffect(() => {
1334 + Scheduler.unstable_yieldValue('Child did commit');
1335 + });
1336 + return null;
1337 + }
1338 +
1339 + function App() {
1340 + const id = useOpaqueIdentifier();
1341 + useEffect(() => {
1342 + Scheduler.unstable_yieldValue('Did commit');
1343 + });
1344 + return (
1345 + <div>
1346 + <div id={id}>Child One</div>
1347 + <RenderedChild />
1348 + <React.Suspense fallback={'Fallback'}>
1349 + <div id={id}>
1350 + <Child text="Child Two" />
1351 + </div>
1352 + </React.Suspense>
1353 + </div>
1354 + );
1355 + }
1356 +
1357 + const container = document.createElement('div');
1358 + document.body.appendChild(container);
1359 +
1360 + container.innerHTML = ReactDOMServer.renderToString(<App />);
1361 +
1362 + suspend = true;
1363 + const root = ReactDOM.createRoot(container, {hydrate: true});
1364 + await ReactTestUtils.act(async () => {
1365 + root.render(<App />);
1366 + });
1367 + jest.runAllTimers();
1368 + expect(Scheduler).toHaveYielded(['Child did commit', 'Did commit']);
1369 + expect(Scheduler).toFlushAndYield([]);
1370 +
1371 + const serverId = container.children[0].children[0].getAttribute('id');
1372 + expect(container.children[0].children.length).toEqual(1);
1373 + expect(
1374 + container.children[0].children[0].getAttribute('id'),
1375 + ).not.toBeNull();
1376 +
1377 + await ReactTestUtils.act(async () => {
1378 + suspend = false;
1379 + resolve();
1380 + await promise;
1381 + });
1382 +
1383 + expect(Scheduler).toHaveYielded(['Child did commit', 'Did commit']);
1384 + expect(Scheduler).toFlushAndYield([]);
1385 + jest.runAllTimers();
1386 +
1387 + expect(container.children[0].children.length).toEqual(2);
1388 + expect(container.children[0].children[0].getAttribute('id')).toEqual(
1389 + container.children[0].children[1].getAttribute('id'),
1390 + );
1391 + expect(
1392 + container.children[0].children[0].getAttribute('id'),
1393 + ).not.toEqual(serverId);
1394 + expect(
1395 + container.children[0].children[0].getAttribute('id'),
1396 + ).not.toBeNull();
1397 + });
1398 +
1399 + it('useOpaqueIdentifier warn when there is a hydration error', async () => {
1400 + function Child({appId}) {
1401 + return <div aria-labelledby={appId} />;
1402 + }
1403 + function App() {
1404 + const id = useOpaqueIdentifier();
1405 + return <Child appId={id} />;
1406 + }
1407 +
1408 + const container = document.createElement('div');
1409 + document.body.appendChild(container);
1410 +
1411 + // This is the wrong HTML string
1412 + container.innerHTML = '<span></span>';
1413 + ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
1414 + expect(() =>
1415 + expect(() => Scheduler.unstable_flushAll()).toThrow(),
1416 + ).toErrorDev([
1417 + 'Warning: Expected server HTML to contain a matching <div> in <div>.',
1418 + ]);
1419 + });
1420 +
1421 + it('useOpaqueIdentifier: IDs match when part of the DOM tree is server rendered and part is client rendered', async () => {
1422 + let suspend = true;
1423 +
1424 + function Child({text}) {
1425 + if (suspend) {
1426 + throw new Promise(() => {});
1427 + } else {
1428 + return text;
1429 + }
1430 + }
1431 +
1432 + function RenderedChild() {
1433 + useEffect(() => {
1434 + Scheduler.unstable_yieldValue('Child did commit');
1435 + });
1436 + return null;
1437 + }
1438 +
1439 + function App() {
1440 + const id = useOpaqueIdentifier();
1441 + useEffect(() => {
1442 + Scheduler.unstable_yieldValue('Did commit');
1443 + });
1444 + return (
1445 + <div>
1446 + <div id={id}>Child One</div>
1447 + <RenderedChild />
1448 + <React.Suspense fallback={'Fallback'}>
1449 + <div id={id}>
1450 + <Child text="Child Two" />
1451 + </div>
1452 + </React.Suspense>
1453 + </div>
1454 + );
1455 + }
1456 +
1457 + const container = document.createElement('div');
1458 + document.body.appendChild(container);
1459 +
1460 + container.innerHTML = ReactDOMServer.renderToString(<App />);
1461 +
1462 + suspend = false;
1463 + const root = ReactDOM.createRoot(container, {hydrate: true});
1464 + await ReactTestUtils.act(async () => {
1465 + root.render(<App />);
1466 + });
1467 + jest.runAllTimers();
1468 + expect(Scheduler).toHaveYielded([
1469 + 'Child did commit',
1470 + 'Did commit',
1471 + 'Child did commit',
1472 + 'Did commit',
1473 + ]);
1474 + expect(Scheduler).toFlushAndYield([]);
1475 +
1476 + expect(container.children[0].children.length).toEqual(2);
1477 + expect(container.children[0].children[0].getAttribute('id')).toEqual(
1478 + container.children[0].children[1].getAttribute('id'),
1479 + );
1480 + expect(
1481 + container.children[0].children[0].getAttribute('id'),
1482 + ).not.toBeNull();
1483 + });
1484 +
1485 + it('useOpaqueIdentifier warn when there is a hydration error', async () => {
1486 + function Child({appId}) {
1487 + return <div aria-labelledby={appId} />;
1488 + }
1489 + function App() {
1490 + const id = useOpaqueIdentifier();
1491 + return <Child appId={id} />;
1492 + }
1493 +
1494 + const container = document.createElement('div');
1495 + document.body.appendChild(container);
1496 +
1497 + // This is the wrong HTML string
1498 + container.innerHTML = '<span></span>';
1499 + ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
1500 + expect(() =>
1501 + expect(() => Scheduler.unstable_flushAll()).toThrow(),
1502 + ).toErrorDev([
1503 + 'Warning: Expected server HTML to contain a matching <div> in <div>.',
1504 + ]);
1505 + });
1506 +
1507 + it('useOpaqueIdentifier throws when there is a hydration error and we are using ID as a string', async () => {
1508 + function Child({appId}) {
1509 + return <div aria-labelledby={appId + ''} />;
1510 + }
1511 + function App() {
1512 + const id = useOpaqueIdentifier();
1513 + return <Child appId={id} />;
1514 + }
1515 +
1516 + const container = document.createElement('div');
1517 + document.body.appendChild(container);
1518 +
1519 + // This is the wrong HTML string
1520 + container.innerHTML = '<span></span>';
1521 + ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
1522 + expect(() =>
1523 + expect(() => Scheduler.unstable_flushAll()).toThrow(
1524 + 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1525 + 'Do not read the value directly.',
1526 + ),
1527 + ).toErrorDev(
1528 + [
1529 + 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
1530 + 'Warning: Did not expect server HTML to contain a <span> in <div>.',
1531 + ],
1532 + {withoutStack: 1},
1533 + );
1534 + });
1535 +
1536 + it('useOpaqueIdentifier throws when there is a hydration error and we are using ID as a string', async () => {
1537 + function Child({appId}) {
1538 + return <div aria-labelledby={appId + ''} />;
1539 + }
1540 + function App() {
1541 + const id = useOpaqueIdentifier();
1542 + return <Child appId={id} />;
1543 + }
1544 +
1545 + const container = document.createElement('div');
1546 + document.body.appendChild(container);
1547 +
1548 + // This is the wrong HTML string
1549 + container.innerHTML = '<span></span>';
1550 + ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
1551 + expect(() =>
1552 + expect(() => Scheduler.unstable_flushAll()).toThrow(
1553 + 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1554 + 'Do not read the value directly.',
1555 + ),
1556 + ).toErrorDev(
1557 + [
1558 + 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
1559 + 'Warning: Did not expect server HTML to contain a <span> in <div>.',
1560 + ],
1561 + {withoutStack: 1},
1562 + );
1563 + });
1564 +
1565 + it('useOpaqueIdentifier throws if you try to use the result as a string in a child component', async () => {
1566 + function Child({appId}) {
1567 + return <div aria-labelledby={appId + ''} />;
1568 + }
1569 + function App() {
1570 + const id = useOpaqueIdentifier();
1571 + return <Child appId={id} />;
1572 + }
1573 +
1574 + const container = document.createElement('div');
1575 + document.body.appendChild(container);
1576 +
1577 + container.innerHTML = ReactDOMServer.renderToString(<App />);
1578 + ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
1579 + expect(() =>
1580 + expect(() => Scheduler.unstable_flushAll()).toThrow(
1581 + 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1582 + 'Do not read the value directly.',
1583 + ),
1584 + ).toErrorDev(
1585 + [
1586 + 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
1587 + 'Warning: Did not expect server HTML to contain a <div> in <div>.',
1588 + ],
1589 + {withoutStack: 1},
1590 + );
1591 + });
1592 +
1593 + it('useOpaqueIdentifier throws if you try to use the result as a string', async () => {
1594 + function App() {
1595 + const id = useOpaqueIdentifier();
1596 + return <div aria-labelledby={id + ''} />;
1597 + }
1598 +
1599 + const container = document.createElement('div');
1600 + document.body.appendChild(container);
1601 +
1602 + container.innerHTML = ReactDOMServer.renderToString(<App />);
1603 + ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
1604 + expect(() =>
1605 + expect(() => Scheduler.unstable_flushAll()).toThrow(
1606 + 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1607 + 'Do not read the value directly.',
1608 + ),
1609 + ).toErrorDev(
1610 + [
1611 + 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
1612 + 'Warning: Did not expect server HTML to contain a <div> in <div>.',
1613 + ],
1614 + {withoutStack: 1},
1615 + );
1616 + });
1617 +
1618 + it('useOpaqueIdentifier throws if you try to use the result as a string in a child component wrapped in a Suspense', async () => {
1619 + function Child({appId}) {
1620 + return <div aria-labelledby={appId + ''} />;
1621 + }
1622 + function App() {
1623 + const id = useOpaqueIdentifier();
1624 + return (
1625 + <React.Suspense fallback={null}>
1626 + <Child appId={id} />
1627 + </React.Suspense>
1628 + );
1629 + }
1630 +
1631 + const container = document.createElement('div');
1632 + document.body.appendChild(container);
1633 +
1634 + container.innerHTML = ReactDOMServer.renderToString(<App />);
1635 +
1636 + ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
1637 +
1638 + expect(() =>
1639 + expect(() => Scheduler.unstable_flushAll()).toThrow(
1640 + 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1641 + 'Do not read the value directly.',
1642 + ),
1643 + ).toErrorDev([
1644 + 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1645 + 'Do not read the value directly.',
1646 + ]);
1647 + });
1648 +
1649 + it('useOpaqueIdentifier throws if you try to add the result as a number in a child component wrapped in a Suspense', async () => {
1650 + function Child({appId}) {
1651 + return <div aria-labelledby={+appId} />;
1652 + }
1653 + function App() {
1654 + const [show] = useState(false);
1655 + const id = useOpaqueIdentifier();
1656 + return (
1657 + <React.Suspense fallback={null}>
1658 + {show && <div id={id} />}
1659 + <Child appId={id} />
1660 + </React.Suspense>
1661 + );
1662 + }
1663 +
1664 + const container = document.createElement('div');
1665 + document.body.appendChild(container);
1666 +
1667 + container.innerHTML = ReactDOMServer.renderToString(<App />);
1668 +
1669 + ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
1670 +
1671 + expect(() =>
1672 + expect(() => Scheduler.unstable_flushAll()).toThrow(
1673 + 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1674 + 'Do not read the value directly.',
1675 + ),
1676 + ).toErrorDev([
1677 + 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1678 + 'Do not read the value directly.',
1679 + ]);
1680 + });
1681 +
1682 + it('useOpaqueIdentifier with two opaque identifiers on the same page', () => {
1683 + let _setShow;
1684 +
1685 + function App() {
1686 + const id1 = useOpaqueIdentifier();
1687 + const id2 = useOpaqueIdentifier();
1688 + const [show, setShow] = useState(true);
1689 + _setShow = setShow;
1690 +
1691 + return (
1692 + <div>
1693 + <React.Suspense fallback={null}>
1694 + {show ? (
1695 + <span id={id1}>{'Child'}</span>
1696 + ) : (
1697 + <span id={id2}>{'Child'}</span>
1698 + )}
1699 + </React.Suspense>
1700 + <span aria-labelledby={id1}>{'test'}</span>
1701 + </div>
1702 + );
1703 + }
1704 +
1705 + const container = document.createElement('div');
1706 + document.body.appendChild(container);
1707 +
1708 + container.innerHTML = ReactDOMServer.renderToString(<App />);
1709 +
1710 + const serverID = container
1711 + .getElementsByTagName('span')[0]
1712 + .getAttribute('id');
1713 + expect(serverID).not.toBeNull();
1714 + expect(
1715 + container
1716 + .getElementsByTagName('span')[1]
1717 + .getAttribute('aria-labelledby'),
1718 + ).toEqual(serverID);
1719 +
1720 + ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
1721 + jest.runAllTimers();
1722 + expect(Scheduler).toHaveYielded([]);
1723 + expect(Scheduler).toFlushAndYield([]);
1724 +
1725 + ReactTestUtils.act(() => {
1726 + _setShow(false);
1727 + });
1728 +
1729 + expect(
1730 + container
1731 + .getElementsByTagName('span')[1]
1732 + .getAttribute('aria-labelledby'),
1733 + ).toEqual(serverID);
1734 + expect(
1735 + container.getElementsByTagName('span')[0].getAttribute('id'),
1736 + ).not.toEqual(serverID);
1737 + expect(
1738 + container.getElementsByTagName('span')[0].getAttribute('id'),
1739 + ).not.toBeNull();
1740 + });
1741 + });
1742 + }
1743 });
packages/react-dom/src/client/DOMPropertyOperations.js
+8
@@ -20,6 +20,7 @@ import {
20 disableJavaScriptURLs,
21 enableTrustedTypesIntegration,
22 } from 'shared/ReactFeatureFlags';
23 +import {isOpaqueHydratingObject} from './ReactDOMHostConfig';
24
25 import type {PropertyInfo} from '../shared/DOMProperty';
26
@@ -107,6 +108,13 @@ export function getValueForAttribute(
108 if (!isAttributeNameSafe(name)) {
109 return;
110 }
111 +
112 + // If the object is an opaque reference ID, it's expected that
113 + // the next prop is different than the server value, so just return
114 + // expected
115 + if (isOpaqueHydratingObject(expected)) {
116 + return expected;
117 + }
118 if (!node.hasAttribute(name)) {
119 return expected === undefined ? undefined : null;
120 }
packages/react-dom/src/client/ReactDOMComponent.js
+10
@@ -84,6 +84,7 @@ import possibleStandardNames from '../shared/possibleStandardNames';
84 import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook';
85 import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook';
86 import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
87 +import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols';
88
89 import {
90 enableDeprecatedFlareAPI,
@@ -849,6 +850,15 @@ export function diffProperties(
850 // to update this element.
851 updatePayload = [];
852 }
853 + } else if (
854 + typeof nextProp === 'object' &&
855 + nextProp !== null &&
856 + nextProp.$$typeof === REACT_OPAQUE_ID_TYPE
857 + ) {
858 + // If we encounter useOpaqueReference's opaque object, this means we are hydrating.
859 + // In this case, call the opaque object's toString function which generates a new client
860 + // ID so client and server IDs match and throws to rerender.
861 + nextProp.toString();
862 } else {
863 // For any other property we always add it to the queue and then we
864 // filter it out using the whitelist during the commit.
packages/react-dom/src/client/ReactDOMHostConfig.js
+50
@@ -58,6 +58,7 @@ import {
58 } from '../shared/HTMLNodeType';
59 import dangerousStyleValue from '../shared/dangerousStyleValue';
60
61 +import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols';
62 import {
63 mountEventResponder,
64 unmountEventResponder,
@@ -150,6 +151,13 @@ export type TimeoutHandle = TimeoutID;
151 export type NoTimeout = -1;
152 export type RendererInspectionConfig = $ReadOnly<{||}>;
153
154 +export opaque type OpaqueIDType =
155 + | string
156 + | {
157 + toString: () => string | void,
158 + valueOf: () => string | void,
159 + };
160 +
161 type SelectionInformation = {|
162 activeElementDetached: null | HTMLElement,
163 focusedElem: null | HTMLElement,
@@ -1147,6 +1155,48 @@ export function getInstanceFromNode(node: HTMLElement): null | Object {
1155 return getClosestInstanceFromNode(node) || null;
1156 }
1157
1158 +let clientId: number = 0;
1159 +export function makeClientId(): OpaqueIDType {
1160 + return 'r:' + (clientId++).toString(36);
1161 +}
1162 +
1163 +export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
1164 + const id = 'r:' + (clientId++).toString(36);
1165 + return {
1166 + toString() {
1167 + warnOnAccessInDEV();
1168 + return id;
1169 + },
1170 + valueOf() {
1171 + warnOnAccessInDEV();
1172 + return id;
1173 + },
1174 + };
1175 +}
1176 +
1177 +let serverId: number = 0;
1178 +export function makeServerId(): OpaqueIDType {
1179 + return 'R:' + (serverId++).toString(36);
1180 +}
1181 +
1182 +export function isOpaqueHydratingObject(value: mixed): boolean {
1183 + return (
1184 + value !== null &&
1185 + typeof value === 'object' &&
1186 + value.$$typeof === REACT_OPAQUE_ID_TYPE
1187 + );
1188 +}
1189 +
1190 +export function makeOpaqueHydratingObject(
1191 + attemptToReadValue: () => void,
1192 +): OpaqueIDType {
1193 + return {
1194 + $$typeof: REACT_OPAQUE_ID_TYPE,
1195 + toString: attemptToReadValue,
1196 + valueOf: attemptToReadValue,
1197 + };
1198 +}
1199 +
1200 export function registerEvent(
1201 event: ReactDOMListenerEvent,
1202 rootContainerInstance: Container,
packages/react-dom/src/server/ReactPartialRendererHooks.js
+8
@@ -12,6 +12,8 @@ import type {
12 TimeoutConfig,
13 } from 'react-reconciler/src/ReactFiberHooks';
14 import type {ThreadID} from './ReactThreadIDAllocator';
15 +import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig';
16 +
17 import type {
18 MutableSource,
19 MutableSourceGetSnapshotFn,
@@ -23,6 +25,7 @@ import type {SuspenseConfig} from 'react-reconciler/src/ReactFiberSuspenseConfig
25 import type {ReactDOMListenerMap} from '../shared/ReactDOMTypes';
26
27 import {validateContextBounds} from './ReactPartialRendererContext';
28 +import {makeServerId} from '../client/ReactDOMHostConfig';
29
30 import invariant from 'shared/invariant';
31 import is from 'shared/objectIs';
@@ -491,6 +494,10 @@ function useTransition(
494 return [startTransition, false];
495 }
496
497 +function useOpaqueIdentifier(): OpaqueIDType {
498 + return makeServerId();
499 +}
500 +
501 function useEvent(event: any): ReactDOMListenerMap {
502 return {
503 clear: noop,
@@ -525,6 +532,7 @@ export const Dispatcher: DispatcherType = {
532 useDeferredValue,
533 useTransition,
534 useEvent,
535 + useOpaqueIdentifier,
536 // Subscriptions are not setup in a server environment.
537 useMutableSource,
538 };
packages/react-native-renderer/src/ReactFabricHostConfig.js
+23
@@ -80,6 +80,7 @@ export type NoTimeout = -1;
80 export type ReactListenerEvent = Object;
81 export type ReactListenerMap = Object;
82 export type ReactListener = Object;
83 +export type OpaqueIDType = void;
84
85 export type RendererInspectionConfig = $ReadOnly<{|
86 // Deprecated. Replaced with getInspectorDataForViewAtPoint.
@@ -486,6 +487,28 @@ export function beforeRemoveInstance(instance: any) {
487 // noop
488 }
489
490 +export function isOpaqueHydratingObject(value: mixed): boolean {
491 + throw new Error('Not yet implemented');
492 +}
493 +
494 +export function makeOpaqueHydratingObject(
495 + attemptToReadValue: () => void,
496 +): OpaqueIDType {
497 + throw new Error('Not yet implemented.');
498 +}
499 +
500 +export function makeClientId(): OpaqueIDType {
501 + throw new Error('Not yet implemented');
502 +}
503 +
504 +export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
505 + throw new Error('Not yet implemented');
506 +}
507 +
508 +export function makeServerId(): OpaqueIDType {
509 + throw new Error('Not yet implemented');
510 +}
511 +
512 export function registerEvent(event: any, rootContainerInstance: Container) {
513 throw new Error('Not yet implemented.');
514 }
packages/react-native-renderer/src/ReactNativeHostConfig.js
+23
@@ -47,6 +47,7 @@ export type ChildSet = void; // Unused
47
48 export type TimeoutHandle = TimeoutID;
49 export type NoTimeout = -1;
50 +export type OpaqueIDType = void;
51
52 export type RendererInspectionConfig = $ReadOnly<{|
53 // Deprecated. Replaced with getInspectorDataForViewAtPoint.
@@ -535,6 +536,28 @@ export function beforeRemoveInstance(instance: any) {
536 // noop
537 }
538
539 +export function isOpaqueHydratingObject(value: mixed): boolean {
540 + throw new Error('Not yet implemented');
541 +}
542 +
543 +export function makeOpaqueHydratingObject(
544 + attemptToReadValue: () => void,
545 +): OpaqueIDType {
546 + throw new Error('Not yet implemented.');
547 +}
548 +
549 +export function makeClientId(): OpaqueIDType {
550 + throw new Error('Not yet implemented');
551 +}
552 +
553 +export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
554 + throw new Error('Not yet implemented');
555 +}
556 +
557 +export function makeServerId(): OpaqueIDType {
558 + throw new Error('Not yet implemented');
559 +}
560 +
561 export function registerEvent(event: any, rootContainerInstance: Container) {
562 throw new Error('Not yet implemented.');
563 }
packages/react-reconciler/src/ReactCurrentFiber.js
+6
@@ -64,3 +64,9 @@ export function setIsRendering(rendering: boolean) {
64 isRendering = rendering;
65 }
66 }
67 +
68 +export function getIsRendering() {
69 + if (__DEV__) {
70 + return isRendering;
71 + }
72 +}
packages/react-reconciler/src/ReactFiberHooks.js
+145 -6
@@ -23,6 +23,7 @@ import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
23 import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
24 import type {FiberRoot} from './ReactFiberRoot';
25 import type {
26 + OpaqueIDType,
27 ReactListenerEvent,
28 ReactListenerMap,
29 ReactListener,
@@ -33,6 +34,7 @@ import {enableUseEventAPI} from 'shared/ReactFeatureFlags';
34
35 import {markRootExpiredAtTime} from './ReactFiberRoot';
36 import {NoWork, Sync} from './ReactFiberExpirationTime';
37 +import {NoMode, BlockingMode} from './ReactTypeOfMode';
38 import {readContext} from './ReactFiberNewContext';
39 import {createDeprecatedResponderListener} from './ReactFiberDeprecatedEvents';
40 import {
@@ -74,6 +76,12 @@ import {
76 runWithPriority,
77 getCurrentPriorityLevel,
78 } from './SchedulerWithReactIntegration';
79 +import {getIsHydrating} from './ReactFiberHydrationContext';
80 +import {
81 + makeClientId,
82 + makeClientIdInDEV,
83 + makeOpaqueHydratingObject,
84 +} from './ReactFiberHostConfig';
85 import {
86 getLastPendingExpirationTime,
87 getWorkInProgressVersion,
@@ -83,6 +91,7 @@ import {
91 warnAboutMultipleRenderersDEV,
92 } from './ReactMutableSource';
93 import {getRootHostContainer} from './ReactFiberHostContext';
94 +import {getIsRendering} from './ReactCurrentFiber';
95
96 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
97
@@ -132,6 +141,7 @@ export type Dispatcher = {|
141 subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
142 ): Snapshot,
143 useEvent(event: ReactListenerEvent): ReactListenerMap,
144 + useOpaqueIdentifier(): OpaqueIDType | void,
145 |};
146
147 type Update<S, A> = {|
@@ -166,10 +176,13 @@ export type HookType =
176 | 'useDeferredValue'
177 | 'useTransition'
178 | 'useMutableSource'
169 - | 'useEvent';
179 + | 'useEvent'
180 + | 'useOpaqueIdentifier';
181
182 let didWarnAboutMismatchedHooksForComponent;
183 +let didWarnAboutUseOpaqueIdentifier;
184 if (__DEV__) {
185 + didWarnAboutUseOpaqueIdentifier = {};
186 didWarnAboutMismatchedHooksForComponent = new Set();
187 }
188
@@ -564,6 +577,8 @@ export function resetHooksAfterThrow(): void {
577 hookTypesUpdateIndexDev = -1;
578
579 currentHookNameInDev = null;
580 +
581 + isUpdatingOpaqueValueInRenderPhase = false;
582 }
583
584 didScheduleRenderPhaseUpdate = false;
@@ -1569,6 +1584,93 @@ function rerenderTransition(
1584 return [start, isPending];
1585 }
1586
1587 +let isUpdatingOpaqueValueInRenderPhase = false;
1588 +export function getIsUpdatingOpaqueValueInRenderPhaseInDEV(): boolean | void {
1589 + if (__DEV__) {
1590 + return isUpdatingOpaqueValueInRenderPhase;
1591 + }
1592 +}
1593 +
1594 +function warnOnOpaqueIdentifierAccessInDEV(fiber) {
1595 + if (__DEV__) {
1596 + // TODO: Should warn in effects and callbacks, too
1597 + const name = getComponentName(fiber.type) || 'Unknown';
1598 + if (getIsRendering() && !didWarnAboutUseOpaqueIdentifier[name]) {
1599 + console.error(
1600 + 'The object passed back from useOpaqueIdentifier is meant to be ' +
1601 + 'passed through to attributes only. Do not read the ' +
1602 + 'value directly.',
1603 + );
1604 + didWarnAboutUseOpaqueIdentifier[name] = true;
1605 + }
1606 + }
1607 +}
1608 +
1609 +function mountOpaqueIdentifier(): OpaqueIDType | void {
1610 + const makeId = __DEV__
1611 + ? makeClientIdInDEV.bind(
1612 + null,
1613 + warnOnOpaqueIdentifierAccessInDEV.bind(null, currentlyRenderingFiber),
1614 + )
1615 + : makeClientId;
1616 +
1617 + if (getIsHydrating()) {
1618 + let didUpgrade = false;
1619 + const fiber = currentlyRenderingFiber;
1620 + const readValue = () => {
1621 + if (!didUpgrade) {
1622 + // Only upgrade once. This works even inside the render phase because
1623 + // the update is added to a shared queue, which outlasts the
1624 + // in-progress render.
1625 + didUpgrade = true;
1626 + if (__DEV__) {
1627 + isUpdatingOpaqueValueInRenderPhase = true;
1628 + setId(makeId());
1629 + isUpdatingOpaqueValueInRenderPhase = false;
1630 + warnOnOpaqueIdentifierAccessInDEV(fiber);
1631 + } else {
1632 + setId(makeId());
1633 + }
1634 + }
1635 + invariant(
1636 + false,
1637 + 'The object passed back from useOpaqueIdentifier is meant to be ' +
1638 + 'passed through to attributes only. Do not read the value directly.',
1639 + );
1640 + };
1641 + const id = makeOpaqueHydratingObject(readValue);
1642 +
1643 + const setId = mountState(id)[1];
1644 +
1645 + if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) {
1646 + currentlyRenderingFiber.effectTag |= UpdateEffect | PassiveEffect;
1647 + pushEffect(
1648 + HookHasEffect | HookPassive,
1649 + () => {
1650 + setId(makeId());
1651 + },
1652 + undefined,
1653 + null,
1654 + );
1655 + }
1656 + return id;
1657 + } else {
1658 + const id = makeId();
1659 + mountState(id);
1660 + return id;
1661 + }
1662 +}
1663 +
1664 +function updateOpaqueIdentifier(): OpaqueIDType | void {
1665 + const id = updateState(undefined)[0];
1666 + return id;
1667 +}
1668 +
1669 +function rerenderOpaqueIdentifier(): OpaqueIDType | void {
1670 + const id = rerenderState(undefined)[0];
1671 + return id;
1672 +}
1673 +
1674 function dispatchAction<S, A>(
1675 fiber: Fiber,
1676 queue: UpdateQueue<S, A>,
@@ -1848,6 +1950,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
1950 useTransition: throwInvalidHookError,
1951 useMutableSource: throwInvalidHookError,
1952 useEvent: throwInvalidHookError,
1953 + useOpaqueIdentifier: throwInvalidHookError,
1954 };
1955
1956 const HooksDispatcherOnMount: Dispatcher = {
@@ -1868,6 +1971,7 @@ const HooksDispatcherOnMount: Dispatcher = {
1971 useTransition: mountTransition,
1972 useMutableSource: mountMutableSource,
1973 useEvent: mountEventListener,
1974 + useOpaqueIdentifier: mountOpaqueIdentifier,
1975 };
1976
1977 const HooksDispatcherOnUpdate: Dispatcher = {
@@ -1888,6 +1992,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
1992 useTransition: updateTransition,
1993 useMutableSource: updateMutableSource,
1994 useEvent: updateEventListener,
1995 + useOpaqueIdentifier: updateOpaqueIdentifier,
1996 };
1997
1998 const HooksDispatcherOnRerender: Dispatcher = {
@@ -1908,6 +2013,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
2013 useTransition: rerenderTransition,
2014 useMutableSource: updateMutableSource,
2015 useEvent: updateEventListener,
2016 + useOpaqueIdentifier: rerenderOpaqueIdentifier,
2017 };
2018
2019 let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
@@ -1944,7 +2050,6 @@ if (__DEV__) {
2050 ): T {
2051 return readContext(context, observedBits);
2052 },
1947 -
2053 useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
2054 currentHookNameInDev = 'useCallback';
2055 mountHookTypesDev();
@@ -2071,6 +2176,11 @@ if (__DEV__) {
2176 mountHookTypesDev();
2177 return mountEventListener(event);
2178 },
2179 + useOpaqueIdentifier(): OpaqueIDType | void {
2180 + currentHookNameInDev = 'useOpaqueIdentifier';
2181 + mountHookTypesDev();
2182 + return mountOpaqueIdentifier();
2183 + },
2184 };
2185
2186 HooksDispatcherOnMountWithHookTypesInDEV = {
@@ -2080,7 +2190,6 @@ if (__DEV__) {
2190 ): T {
2191 return readContext(context, observedBits);
2192 },
2083 -
2193 useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
2194 currentHookNameInDev = 'useCallback';
2195 updateHookTypesDev();
@@ -2202,6 +2311,11 @@ if (__DEV__) {
2311 updateHookTypesDev();
2312 return mountEventListener(event);
2313 },
2314 + useOpaqueIdentifier(): OpaqueIDType | void {
2315 + currentHookNameInDev = 'useOpaqueIdentifier';
2316 + updateHookTypesDev();
2317 + return mountOpaqueIdentifier();
2318 + },
2319 };
2320
2321 HooksDispatcherOnUpdateInDEV = {
@@ -2211,7 +2325,6 @@ if (__DEV__) {
2325 ): T {
2326 return readContext(context, observedBits);
2327 },
2214 -
2328 useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
2329 currentHookNameInDev = 'useCallback';
2330 updateHookTypesDev();
@@ -2333,6 +2446,11 @@ if (__DEV__) {
2446 updateHookTypesDev();
2447 return updateEventListener(event);
2448 },
2449 + useOpaqueIdentifier(): OpaqueIDType | void {
2450 + currentHookNameInDev = 'useOpaqueIdentifier';
2451 + updateHookTypesDev();
2452 + return updateOpaqueIdentifier();
2453 + },
2454 };
2455
2456 HooksDispatcherOnRerenderInDEV = {
@@ -2464,6 +2582,11 @@ if (__DEV__) {
2582 updateHookTypesDev();
2583 return updateEventListener(event);
2584 },
2585 + useOpaqueIdentifier(): OpaqueIDType | void {
2586 + currentHookNameInDev = 'useOpaqueIdentifier';
2587 + updateHookTypesDev();
2588 + return rerenderOpaqueIdentifier();
2589 + },
2590 };
2591
2592 InvalidNestedHooksDispatcherOnMountInDEV = {
@@ -2474,7 +2597,6 @@ if (__DEV__) {
2597 warnInvalidContextAccess();
2598 return readContext(context, observedBits);
2599 },
2477 -
2600 useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
2601 currentHookNameInDev = 'useCallback';
2602 warnInvalidHookAccess();
@@ -2611,6 +2733,12 @@ if (__DEV__) {
2733 mountHookTypesDev();
2734 return mountEventListener(event);
2735 },
2736 + useOpaqueIdentifier(): OpaqueIDType | void {
2737 + currentHookNameInDev = 'useOpaqueIdentifier';
2738 + warnInvalidHookAccess();
2739 + mountHookTypesDev();
2740 + return mountOpaqueIdentifier();
2741 + },
2742 };
2743
2744 InvalidNestedHooksDispatcherOnUpdateInDEV = {
@@ -2621,7 +2749,6 @@ if (__DEV__) {
2749 warnInvalidContextAccess();
2750 return readContext(context, observedBits);
2751 },
2624 -
2752 useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
2753 currentHookNameInDev = 'useCallback';
2754 warnInvalidHookAccess();
@@ -2758,6 +2885,12 @@ if (__DEV__) {
2885 updateHookTypesDev();
2886 return updateEventListener(event);
2887 },
2888 + useOpaqueIdentifier(): OpaqueIDType | void {
2889 + currentHookNameInDev = 'useOpaqueIdentifier';
2890 + warnInvalidHookAccess();
2891 + updateHookTypesDev();
2892 + return updateOpaqueIdentifier();
2893 + },
2894 };
2895
2896 InvalidNestedHooksDispatcherOnRerenderInDEV = {
@@ -2905,5 +3038,11 @@ if (__DEV__) {
3038 updateHookTypesDev();
3039 return updateEventListener(event);
3040 },
3041 + useOpaqueIdentifier(): OpaqueIDType | void {
3042 + currentHookNameInDev = 'useOpaqueIdentifier';
3043 + warnInvalidHookAccess();
3044 + updateHookTypesDev();
3045 + return rerenderOpaqueIdentifier();
3046 + },
3047 };
3048 }
packages/react-reconciler/src/ReactFiberHydrationContext.js
+5
@@ -485,9 +485,14 @@ function resetHydrationState(): void {
485 isHydrating = false;
486 }
487
488 +function getIsHydrating(): boolean {
489 + return isHydrating;
490 +}
491 +
492 export {
493 warnIfHydrating,
494 enterHydrationState,
495 + getIsHydrating,
496 reenterHydrationStateFromDehydratedSuspenseInstance,
497 resetHydrationState,
498 tryToClaimNextHydratableInstance,
packages/react-reconciler/src/ReactFiberWorkLoop.js
+7 -2
@@ -145,7 +145,11 @@ import {
145 } from './ReactFiberCommitWork';
146 import {enqueueUpdate} from './ReactUpdateQueue';
147 import {resetContextDependencies} from './ReactFiberNewContext';
148 -import {resetHooksAfterThrow, ContextOnlyDispatcher} from './ReactFiberHooks';
148 +import {
149 + resetHooksAfterThrow,
150 + ContextOnlyDispatcher,
151 + getIsUpdatingOpaqueValueInRenderPhaseInDEV,
152 +} from './ReactFiberHooks';
153 import {createCapturedValue} from './ReactCapturedValue';
154
155 import {
@@ -2842,7 +2846,8 @@ function warnAboutRenderPhaseUpdatesInDEV(fiber) {
2846 if (__DEV__) {
2847 if (
2848 ReactCurrentDebugFiberIsRenderingInDEV &&
2845 - (executionContext & RenderContext) !== NoContext
2849 + (executionContext & RenderContext) !== NoContext &&
2850 + !getIsUpdatingOpaqueValueInRenderPhaseInDEV()
2851 ) {
2852 switch (fiber.tag) {
2853 case FunctionComponent:
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+7
@@ -38,6 +38,7 @@ export opaque type ChildSet = mixed; // eslint-disable-line no-undef
38 export opaque type TimeoutHandle = mixed; // eslint-disable-line no-undef
39 export opaque type NoTimeout = mixed; // eslint-disable-line no-undef
40 export opaque type RendererInspectionConfig = mixed; // eslint-disable-line no-undef
41 +export opaque type OpaqueIDType = mixed;
42 export type EventResponder = any;
43 export type ReactListenerEvent = Object;
44 export type ReactListenerMap = Object;
@@ -82,6 +83,12 @@ export const mountEventListener = $$$hostConfig.mountEventListener;
83 export const unmountEventListener = $$$hostConfig.unmountEventListener;
84 export const validateEventListenerTarget =
85 $$$hostConfig.validateEventListenerTarget;
86 +export const isOpaqueHydratingObject = $$$hostConfig.isOpaqueHydratingObject;
87 +export const makeOpaqueHydratingObject =
88 + $$$hostConfig.makeOpaqueHydratingObject;
89 +export const makeClientId = $$$hostConfig.makeClientId;
90 +export const makeClientIdInDEV = $$$hostConfig.makeClientIdInDEV;
91 +export const makeServerId = $$$hostConfig.makeServerId;
92
93 // -------------------
94 // Mutation
packages/react-test-renderer/src/ReactTestHostConfig.js
+49
@@ -14,6 +14,7 @@ import type {
14 } from 'shared/ReactTypes';
15
16 import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags';
17 +import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols';
18
19 export type Type = string;
20 export type Props = Object;
@@ -44,6 +45,12 @@ export type ChildSet = void; // Unused
45 export type TimeoutHandle = TimeoutID;
46 export type NoTimeout = -1;
47 export type EventResponder = any;
48 +export opaque type OpaqueIDType =
49 + | string
50 + | {
51 + toString: () => string | void,
52 + valueOf: () => string | void,
53 + };
54
55 export type ReactListenerEvent = Object;
56 export type ReactListenerMap = Object;
@@ -381,6 +388,48 @@ export function beforeRemoveInstance(instance: any) {
388 // noop
389 }
390
391 +let clientId: number = 0;
392 +export function makeClientId(): OpaqueIDType {
393 + return 'c_' + (clientId++).toString(36);
394 +}
395 +
396 +export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
397 + const id = 'c_' + (clientId++).toString(36);
398 + return {
399 + toString() {
400 + warnOnAccessInDEV();
401 + return id;
402 + },
403 + valueOf() {
404 + warnOnAccessInDEV();
405 + return id;
406 + },
407 + };
408 +}
409 +
410 +let serverId: number = 0;
411 +export function makeServerId(): OpaqueIDType {
412 + return 's_' + (serverId++).toString(36);
413 +}
414 +
415 +export function isOpaqueHydratingObject(value: mixed): boolean {
416 + return (
417 + value !== null &&
418 + typeof value === 'object' &&
419 + value.$$typeof === REACT_OPAQUE_ID_TYPE
420 + );
421 +}
422 +
423 +export function makeOpaqueHydratingObject(
424 + attemptToReadValue: () => void,
425 +): OpaqueIDType {
426 + return {
427 + $$typeof: REACT_OPAQUE_ID_TYPE,
428 + toString: attemptToReadValue,
429 + valueOf: attemptToReadValue,
430 + };
431 +}
432 +
433 export function registerEvent(event: any, rootContainerInstance: Container) {
434 throw new Error('Not yet implemented.');
435 }
packages/react/index.classic.fb.js
+1
@@ -50,5 +50,6 @@ export {
50 DEPRECATED_createResponder,
51 // enableScopeAPI
52 unstable_createScope,
53 + unstable_useOpaqueIdentifier,
54 } from './src/React';
55 export {jsx, jsxs, jsxDEV} from './src/jsx/ReactJSX';
packages/react/index.experimental.js
+1
@@ -45,4 +45,5 @@ export {
45 unstable_withSuspenseConfig,
46 // enableBlocksAPI
47 block,
48 + unstable_useOpaqueIdentifier,
49 } from './src/React';
packages/react/index.js
+1
@@ -76,4 +76,5 @@ export {
76 DEPRECATED_createResponder,
77 unstable_createFundamental,
78 unstable_createScope,
79 + unstable_useOpaqueIdentifier,
80 } from './src/React';
packages/react/index.modern.fb.js
+1
@@ -49,5 +49,6 @@ export {
49 DEPRECATED_createResponder,
50 // enableScopeAPI
51 unstable_createScope,
52 + unstable_useOpaqueIdentifier,
53 } from './src/React';
54 export {jsx, jsxs, jsxDEV} from './src/jsx/ReactJSX';
packages/react/src/React.js
+2
@@ -45,6 +45,7 @@ import {
45 useResponder,
46 useTransition,
47 useDeferredValue,
48 + useOpaqueIdentifier,
49 } from './ReactHooks';
50 import {withSuspenseConfig} from './ReactBatchConfig';
51 import {
@@ -117,4 +118,5 @@ export {
118 createFundamental as unstable_createFundamental,
119 // enableScopeAPI
120 createScope as unstable_createScope,
121 + useOpaqueIdentifier as unstable_useOpaqueIdentifier,
122 };
packages/react/src/ReactHooks.js
+7
@@ -15,6 +15,8 @@ import type {
15 ReactEventResponder,
16 ReactEventResponderListener,
17 } from 'shared/ReactTypes';
18 +import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig';
19 +
20 import invariant from 'shared/invariant';
21 import {REACT_RESPONDER_TYPE} from 'shared/ReactSymbols';
22
@@ -181,6 +183,11 @@ export function useDeferredValue<T>(value: T, config: ?Object): T {
183 return dispatcher.useDeferredValue(value, config);
184 }
185
186 +export function useOpaqueIdentifier(): OpaqueIDType | void {
187 + const dispatcher = resolveDispatcher();
188 + return dispatcher.useOpaqueIdentifier();
189 +}
190 +
191 export function useMutableSource<Source, Snapshot>(
192 source: MutableSource<Source>,
193 getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
packages/shared/ReactSymbols.js
+2
@@ -26,6 +26,7 @@ export let REACT_SERVER_BLOCK_TYPE = 0xeada;
26 export let REACT_FUNDAMENTAL_TYPE = 0xead5;
27 export let REACT_RESPONDER_TYPE = 0xead6;
28 export let REACT_SCOPE_TYPE = 0xead7;
29 +export let REACT_OPAQUE_ID_TYPE = 0xeae0;
30
31 if (typeof Symbol === 'function' && Symbol.for) {
32 const symbolFor = Symbol.for;
@@ -46,6 +47,7 @@ if (typeof Symbol === 'function' && Symbol.for) {
47 REACT_FUNDAMENTAL_TYPE = symbolFor('react.fundamental');
48 REACT_RESPONDER_TYPE = symbolFor('react.responder');
49 REACT_SCOPE_TYPE = symbolFor('react.scope');
50 + REACT_OPAQUE_ID_TYPE = symbolFor('react.opaque.id');
51 }
52
53 const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
scripts/error-codes/codes.json
+2 -1
@@ -351,5 +351,6 @@
351 "351": "Unsupported type.",
352 "352": "React Blocks (and Lazy Components) are expected to be replaced by a compiler on the server. Try configuring your compiler set up and avoid using React.lazy inside of Blocks.",
353 "353": "A server block should never encode any other slots. This is a bug in React.",
354 - "354": "getInspectorDataForViewAtPoint() is not available in production."
354 + "354": "getInspectorDataForViewAtPoint() is not available in production.",
355 + "355": "The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly."
356 }