Skip to content

Commit

Permalink
Add useOpaqueIdentifier Hook (#17322)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
lunaruan and acdlite authored Apr 7, 2020
1 parent 4169420 commit 3278d24
Show file tree
Hide file tree
Showing 24 changed files with 1,370 additions and 9 deletions.
22 changes: 22 additions & 0 deletions packages/react-art/src/ReactARTHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,28 @@ export function beforeRemoveInstance(instance) {
// noop
}

export function isOpaqueHydratingObject(value: mixed): boolean {
throw new Error('Not yet implemented');
}

export function makeOpaqueHydratingObject(
attemptToReadValue: () => void,
): OpaqueIDType {
throw new Error('Not yet implemented.');
}

export function makeClientId(): OpaqueIDType {
throw new Error('Not yet implemented');
}

export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
throw new Error('Not yet implemented');
}

export function makeServerId(): OpaqueIDType {
throw new Error('Not yet implemented');
}

export function registerEvent(event: any, rootContainerInstance: any) {
throw new Error('Not yet implemented.');
}
Expand Down
26 changes: 26 additions & 0 deletions packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ import type {
ReactScopeMethods,
} from 'shared/ReactTypes';
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig';

import type {Hook, TimeoutConfig} from 'react-reconciler/src/ReactFiberHooks';
import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactFiberHooks';
import type {SuspenseConfig} from 'react-reconciler/src/ReactFiberSuspenseConfig';
import {NoMode} from 'react-reconciler/src/ReactTypeOfMode';

import ErrorStackParser from 'error-stack-parser';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols';
import {
FunctionComponent,
SimpleMemoComponent,
Expand Down Expand Up @@ -61,6 +65,8 @@ type Dispatch<A> = A => void;

let primitiveStackCache: null | Map<string, Array<any>> = null;

let currentFiber: Fiber | null = null;

function getPrimitiveStackCache(): Map<string, Array<any>> {
// This initializes a cache of all primitive hooks so that the top
// most stack frames added by calling the primitive hook can be removed.
Expand Down Expand Up @@ -319,6 +325,23 @@ function useDeferredValue<T>(value: T, config: TimeoutConfig | null | void): T {
return value;
}

function useOpaqueIdentifier(): OpaqueIDType | void {
const hook = nextHook(); // State
if (currentFiber && currentFiber.mode === NoMode) {
nextHook(); // Effect
}
let value = hook === null ? undefined : hook.memoizedState;
if (value && value.$$typeof === REACT_OPAQUE_ID_TYPE) {
value = undefined;
}
hookLog.push({
primitive: 'OpaqueIdentifier',
stackError: new Error(),
value,
});
return value;
}

const Dispatcher: DispatcherType = {
readContext,
useCallback,
Expand All @@ -336,6 +359,7 @@ const Dispatcher: DispatcherType = {
useMutableSource,
useDeferredValue,
useEvent,
useOpaqueIdentifier,
};

// Inspect
Expand Down Expand Up @@ -684,6 +708,8 @@ export function inspectHooksOfFiber(
currentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
}

currentFiber = fiber;

if (
fiber.tag !== FunctionComponent &&
fiber.tag !== SimpleMemoComponent &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,64 @@ describe('ReactHooksInspectionIntegration', () => {
},
]);
});

it('should support composite useOpaqueIdentifier hook', () => {
function Foo(props) {
const id = React.unstable_useOpaqueIdentifier();
const [state] = React.useState(() => 'hello', []);
return <div id={id}>{state}</div>;
}

const renderer = ReactTestRenderer.create(<Foo />);
const childFiber = renderer.root.findByType(Foo)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);

expect(tree.length).toEqual(2);

expect(tree[0].id).toEqual(0);
expect(tree[0].isStateEditable).toEqual(false);
expect(tree[0].name).toEqual('OpaqueIdentifier');
expect((tree[0].value + '').startsWith('c_')).toBe(true);

expect(tree[1]).toEqual({
id: 1,
isStateEditable: true,
name: 'State',
value: 'hello',
subHooks: [],
});
});

it('should support composite useOpaqueIdentifier hook in concurrent mode', () => {
function Foo(props) {
const id = React.unstable_useOpaqueIdentifier();
const [state] = React.useState(() => 'hello', []);
return <div id={id}>{state}</div>;
}

const renderer = ReactTestRenderer.create(<Foo />, {
unstable_isConcurrent: true,
});
expect(Scheduler).toFlushWithoutYielding();

const childFiber = renderer.root.findByType(Foo)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);

expect(tree.length).toEqual(2);

expect(tree[0].id).toEqual(0);
expect(tree[0].isStateEditable).toEqual(false);
expect(tree[0].name).toEqual('OpaqueIdentifier');
expect((tree[0].value + '').startsWith('c_')).toBe(true);

expect(tree[1]).toEqual({
id: 1,
isStateEditable: true,
name: 'State',
value: 'hello',
subHooks: [],
});
});
}

describe('useDebugValue', () => {
Expand Down
Loading

0 comments on commit 3278d24

Please sign in to comment.