Skip to content

Commit

Permalink
Implements useId hook for Flight server.
Browse files Browse the repository at this point in the history
The approach for ids for Flight is different from Fizz/Client where there is a need for determinancy. Flight rendered elements will not be rendered on the client and as such the ids generated in a request only need to be unique. However since FLight does support refetching subtrees it is possible a client will need to patch up a part of the tree rather than replacing the entire thing so it is not safe to use a simple incrementing counter. To solve for this we allow the caller to specify a prefix. On an initial fetch it is likely this will be empty but on refetches or subtrees we expect to have a client `useId` provide the prefix since it will guaranteed be unique for that subtree and thus for the entire tree. It is also possible that we will automatically provide prefixes based on a client/Fizz useId on refetches

in addition to the core change I also modified the structure of options for renderToReadableStream where `onError`, `context`, and the new `identifierPrefix` are properties of an Options object argument to avoid the clumsiness of a growing list of optional function arguments.
  • Loading branch information
gnoff committed Mar 28, 2022
1 parent e7d0053 commit 39e8147
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 20 deletions.
60 changes: 56 additions & 4 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,59 @@ describe('ReactFlight', () => {
);
});

describe('Hooks', () => {
function DivWithId() {
const id = React.useId();
return <div prop={id} />;
}

it('should support useId', () => {
function App() {
return (
<>
<DivWithId />
<DivWithId />
</>
);
}

const transport = ReactNoopFlightServer.render(<App />);
act(() => {
ReactNoop.render(ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(
<>
<div prop=":$:F1" />
<div prop=":$:F2" />
</>,
);
});

it('accepts an identifier prefix that prefixes generated ids', () => {
function App() {
return (
<>
<DivWithId />
<DivWithId />
</>
);
}

const transport = ReactNoopFlightServer.render(<App />, {
identifierPrefix: 'foo',
});
act(() => {
ReactNoop.render(ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(
<>
<div prop=":foo:F1" />
<div prop=":foo:F2" />
</>,
);
});
});

describe('ServerContext', () => {
// @gate enableServerContext
it('supports basic createServerContext usage', () => {
Expand Down Expand Up @@ -759,15 +812,14 @@ describe('ReactFlight', () => {
function Bar() {
return <span>{React.useContext(ServerContext)}</span>;
}
const transport = ReactNoopFlightServer.render(<Bar />, {}, [
['ServerContext', 'Override'],
]);
const transport = ReactNoopFlightServer.render(<Bar />, {
context: [['ServerContext', 'Override']],
});

act(() => {
const flightModel = ReactNoopFlightClient.read(transport);
ReactNoop.render(flightModel);
});

expect(ReactNoop).toMatchRenderedOutput(<span>Override</span>);
});

Expand Down
11 changes: 5 additions & 6 deletions packages/react-noop-renderer/src/ReactNoopFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,20 +61,19 @@ const ReactNoopFlightServer = ReactFlightServer({

type Options = {
onError?: (error: mixed) => void,
context?: Array<[string, ServerContextJSONValue]>,
identifierPrefix?: string,
};

function render(
model: ReactModel,
options?: Options,
context?: Array<[string, ServerContextJSONValue]>,
): Destination {
function render(model: ReactModel, options?: Options): Destination {
const destination: Destination = [];
const bundlerConfig = undefined;
const request = ReactNoopFlightServer.createRequest(
model,
bundlerConfig,
options ? options.onError : undefined,
context,
options ? options.context : undefined,
options ? options.identifierPrefix : undefined,
);
ReactNoopFlightServer.startWork(request);
ReactNoopFlightServer.startFlowing(request, destination);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {

type Options = {
onError?: (error: mixed) => void,
identifierPrefix?: string,
};

function render(
Expand All @@ -33,6 +34,8 @@ function render(
model,
config,
options ? options.onError : undefined,
undefined, // not currently set up to supply context overrides
options ? options.identifierPrefix : undefined,
);
startWork(request);
startFlowing(request, destination);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,21 @@ import {

type Options = {
onError?: (error: mixed) => void,
context?: Array<[string, ServerContextJSONValue]>,
identifierPrefix?: string,
};

function renderToReadableStream(
model: ReactModel,
webpackMap: BundlerConfig,
options?: Options,
context?: Array<[string, ServerContextJSONValue]>,
): ReadableStream {
const request = createRequest(
model,
webpackMap,
options ? options.onError : undefined,
context,
options ? options.context : undefined,
options ? options.identifierPrefix : undefined,
);
const stream = new ReadableStream({
type: 'bytes',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ function createDrainHandler(destination, request) {

type Options = {
onError?: (error: mixed) => void,
context?: Array<[string, ServerContextJSONValue]>,
identifierPrefix?: string,
};

type Controls = {|
Expand All @@ -34,13 +36,13 @@ function renderToPipeableStream(
model: ReactModel,
webpackMap: BundlerConfig,
options?: Options,
context?: Array<[string, ServerContextJSONValue]>,
): Controls {
const request = createRequest(
model,
webpackMap,
options ? options.onError : undefined,
context,
options ? options.context : undefined,
options ? options.identifierPrefix : undefined,
);
let hasStartedFlowing = false;
startWork(request);
Expand Down
28 changes: 27 additions & 1 deletion packages/react-server/src/ReactFlightHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,27 @@
*/

import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes';
import type {Request} from './ReactFlightServer';
import type {ReactServerContext} from 'shared/ReactTypes';
import {REACT_SERVER_CONTEXT_TYPE} from 'shared/ReactSymbols';
import {readContext as readContextImpl} from './ReactFlightNewContext';

let currentIndentifierPrefix = '$';
let currentIndentifierCount = 0;

export function prepareToUseHooksForRequest(request: Request) {
if (request.identifierPrefix) {
currentIndentifierPrefix = request.identifierPrefix;
}
currentIndentifierCount = request.identifierCount;
}

export function resetHooksForRequest(request: Request) {
currentIndentifierPrefix = '$';
request.identifierCount = currentIndentifierCount;
currentIndentifierCount = 0;
}

function readContext<T>(context: ReactServerContext<T>): T {
if (__DEV__) {
if (context.$$typeof !== REACT_SERVER_CONTEXT_TYPE) {
Expand Down Expand Up @@ -61,7 +78,7 @@ export const Dispatcher: DispatcherType = {
useLayoutEffect: (unsupportedHook: any),
useImperativeHandle: (unsupportedHook: any),
useEffect: (unsupportedHook: any),
useId: (unsupportedHook: any),
useId,
useMutableSource: (unsupportedHook: any),
useSyncExternalStore: (unsupportedHook: any),
useCacheRefresh(): <T>(?() => T, ?T) => void {
Expand Down Expand Up @@ -91,3 +108,12 @@ export function setCurrentCache(cache: Map<Function, mixed> | null) {
export function getCurrentCache() {
return currentCache;
}

function useId(): string {
return (
':' +
currentIndentifierPrefix +
':F' +
(currentIndentifierCount++).toString(32)
);
}
19 changes: 14 additions & 5 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ import {
isModuleReference,
} from './ReactFlightServerConfig';

import {Dispatcher, getCurrentCache, setCurrentCache} from './ReactFlightHooks';
import {
Dispatcher,
getCurrentCache,
prepareToUseHooksForRequest,
resetHooksForRequest,
setCurrentCache,
} from './ReactFlightHooks';
import {
pushProvider,
popProvider,
Expand Down Expand Up @@ -102,14 +108,12 @@ export type Request = {
writtenSymbols: Map<Symbol, number>,
writtenModules: Map<ModuleKey, number>,
writtenProviders: Map<string, number>,
identifierPrefix?: string,
identifierCount: number,
onError: (error: mixed) => void,
toJSON: (key: string, value: ReactModel) => ReactJSONValue,
};

export type Options = {
onError?: (error: mixed) => void,
};

const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;

function defaultErrorHandler(error: mixed) {
Expand All @@ -126,6 +130,7 @@ export function createRequest(
bundlerConfig: BundlerConfig,
onError: void | ((error: mixed) => void),
context?: Array<[string, ServerContextJSONValue]>,
identifierPrefix?: string,
): Request {
const pingedSegments = [];
const request = {
Expand All @@ -143,6 +148,8 @@ export function createRequest(
writtenSymbols: new Map(),
writtenModules: new Map(),
writtenProviders: new Map(),
identifierPrefix,
identifierCount: 1,
onError: onError === undefined ? defaultErrorHandler : onError,
toJSON: function(key: string, value: ReactModel): ReactJSONValue {
return resolveModelToJSON(request, this, key, value);
Expand Down Expand Up @@ -826,6 +833,7 @@ function performWork(request: Request): void {
const prevCache = getCurrentCache();
ReactCurrentDispatcher.current = Dispatcher;
setCurrentCache(request.cache);
prepareToUseHooksForRequest(request);

try {
const pingedSegments = request.pingedSegments;
Expand All @@ -843,6 +851,7 @@ function performWork(request: Request): void {
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
setCurrentCache(prevCache);
resetHooksForRequest(request);
}
}

Expand Down

0 comments on commit 39e8147

Please sign in to comment.