Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
f75b5d3
Flight side of server context
salazarm Feb 7, 2022
29e80db
1 more test
salazarm Feb 7, 2022
54e8812
rm unused function
salazarm Feb 7, 2022
e148a0a
flow+prettier
salazarm Feb 14, 2022
532dfa2
flow again =)
salazarm Feb 14, 2022
f2868c2
duplicate ReactServerContext across packages
salazarm Feb 15, 2022
a861492
store default value when lazily initializing server context
salazarm Feb 17, 2022
fb521be
.
salazarm Feb 17, 2022
a7051aa
better comment
salazarm Feb 17, 2022
066b206
derp... missing import
salazarm Feb 17, 2022
7706ed7
rm optional chaining
salazarm Feb 17, 2022
8bb4359
missed feature flag
salazarm Feb 17, 2022
03f08ba
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED ??
salazarm Feb 17, 2022
9948c43
add warning if non ServerContext passed into useServerContext
salazarm Feb 17, 2022
0bb2d55
pass context in as array of arrays
salazarm Feb 17, 2022
0dc363f
make importServerContext nott pollute the global context state
salazarm Feb 23, 2022
637ad8b
Merge branch 'main' into flightServerContext
salazarm Feb 23, 2022
be811ec
merge main
salazarm Feb 23, 2022
3c8ec09
remove useServerContext
salazarm Feb 23, 2022
9c7e061
dont rely on object getters in ReactServerContext and disallow JSX
salazarm Feb 24, 2022
6f71944
add symbols to devtools + rename globalServerContextRegistry to just …
salazarm Feb 24, 2022
d74b7bd
gate test case as experimental
salazarm Feb 24, 2022
4d9d014
feedback
salazarm Mar 2, 2022
de7f685
remove unions
salazarm Mar 2, 2022
079691e
Lint
salazarm Mar 2, 2022
cb66687
fix oopsies (tests/lint/mismatching arguments/signatures
salazarm Mar 2, 2022
0946c5d
lint again
salazarm Mar 2, 2022
64c6477
replace-fork
salazarm Mar 2, 2022
1f5e888
remove extraneous change
salazarm Mar 2, 2022
8323eb8
rebase
salazarm Feb 7, 2022
640c4a8
1 more test
salazarm Feb 7, 2022
c83c3cc
rm unused function
salazarm Feb 7, 2022
64621f0
flow+prettier
salazarm Feb 14, 2022
905d184
flow again =)
salazarm Feb 14, 2022
42ca798
duplicate ReactServerContext across packages
salazarm Feb 15, 2022
4457295
store default value when lazily initializing server context
salazarm Feb 17, 2022
a69065b
.
salazarm Feb 17, 2022
ef20b40
better comment
salazarm Feb 17, 2022
c5a58d3
derp... missing import
salazarm Feb 17, 2022
6124a34
rm optional chaining
salazarm Feb 17, 2022
fd1465e
missed feature flag
salazarm Feb 17, 2022
ebe8a1e
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED ??
salazarm Feb 17, 2022
4e8a2d2
add warning if non ServerContext passed into useServerContext
salazarm Feb 17, 2022
72dbc55
pass context in as array of arrays
salazarm Feb 17, 2022
6e4ed97
make importServerContext nott pollute the global context state
salazarm Feb 23, 2022
c5988f2
merge main
salazarm Feb 23, 2022
5397db6
remove useServerContext
salazarm Feb 23, 2022
9d30d7b
dont rely on object getters in ReactServerContext and disallow JSX
salazarm Feb 24, 2022
50594a2
add symbols to devtools + rename globalServerContextRegistry to just …
salazarm Feb 24, 2022
43a71b2
gate test case as experimental
salazarm Feb 24, 2022
69a9ccd
feedback
salazarm Mar 2, 2022
bc03997
remove unions
salazarm Mar 2, 2022
499f20b
Lint
salazarm Mar 2, 2022
b5e5e47
fix oopsies (tests/lint/mismatching arguments/signatures
salazarm Mar 2, 2022
90f6f08
lint again
salazarm Mar 2, 2022
672712e
replace-fork
salazarm Mar 2, 2022
03fb89b
remove extraneous change
salazarm Mar 2, 2022
db992af
rebase
salazarm Mar 3, 2022
9ceb955
reinline
salazarm Mar 3, 2022
d0d8f5d
Merge branch 'flightServerContext' of github.com:salazarm/react into …
salazarm Mar 3, 2022
eafb265
rebase
salazarm Mar 3, 2022
5b937ad
add back changes lost due to rebase being hard
salazarm Mar 3, 2022
06b6008
emit chunk for provider
salazarm Mar 3, 2022
c83bc66
remove case for React provider type
salazarm Mar 3, 2022
534d370
update type for SomeChunk
salazarm Mar 3, 2022
8283b4b
enable flag with experimental
salazarm Mar 3, 2022
c596825
add missing types
salazarm Mar 3, 2022
04293a0
fix flow type
salazarm Mar 3, 2022
4b780b5
missing type
salazarm Mar 4, 2022
dc4081d
t: any
salazarm Mar 4, 2022
ff6269f
revert extraneous type change
salazarm Mar 4, 2022
f5a8b25
better type
salazarm Mar 4, 2022
a714680
better type
salazarm Mar 4, 2022
81f798f
feedback
salazarm Mar 7, 2022
72ed1cc
Merge branch 'main' of github.com:salazarm/react into flightServerCon…
salazarm Mar 7, 2022
4b808b1
change import to type import
salazarm Mar 7, 2022
463047c
test?
salazarm Mar 7, 2022
3d16208
test?
salazarm Mar 7, 2022
1c4eaf9
remove react-dom
salazarm Mar 7, 2022
af300fc
remove react-native-renderer from react-server-native-relay/package.json
salazarm Mar 7, 2022
9333935
gate change in FiberNewContext, getComponentNameFromType, use switch …
salazarm Mar 8, 2022
2d5129c
getComponentNameFromTpe: server context type gated and use displayNam…
salazarm Mar 8, 2022
573d394
fallthrough
salazarm Mar 8, 2022
26c5667
lint....
salazarm Mar 8, 2022
83fcb8b
Merge branch 'main' into flightServerContext
salazarm Mar 8, 2022
278499d
POP
salazarm Mar 8, 2022
b6bbe30
lint
salazarm Mar 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ import {
parseModel,
} from './ReactFlightClientHostConfig';

import {REACT_LAZY_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
import {getOrCreateServerContext} from 'react/src/ReactServerContext';

import {
REACT_LAZY_TYPE,
REACT_ELEMENT_TYPE,
REACT_PROVIDER_TYPE,
} from 'shared/ReactSymbols';

export type JSONValue =
| number
Expand Down Expand Up @@ -318,6 +324,11 @@ export function parseModelString(
// When passed into React, we'll know how to suspend on this.
return createLazyChunkWrapper(chunk);
}
case '!': {
if (value === '!') {
return REACT_PROVIDER_TYPE;
}
}
}
return value;
}
Expand All @@ -327,10 +338,18 @@ export function parseModelTuple(
value: {+[key: string]: JSONValue} | $ReadOnlyArray<JSONValue>,
): any {
const tuple: [mixed, mixed, mixed, mixed] = (value: any);
if (tuple[0] === REACT_ELEMENT_TYPE) {
// TODO: Consider having React just directly accept these arrays as elements.
// Or even change the ReactElement type to be an array.
return createElement(tuple[1], tuple[2], tuple[3]);

switch (tuple[0]) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to have a switch here I guess.

case REACT_ELEMENT_TYPE:
// TODO: Consider having React just directly accept these arrays as elements.
// Or even change the ReactElement type to be an array.
return createElement(tuple[1], tuple[2], tuple[3]);
case REACT_PROVIDER_TYPE:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So technically, this symbol represents that this object is an instance of the Context's Provider type. Not the instance of the Element containing that object.

This matters because technically I should be able to pass the provider itself through props from server to client.

<ClientComponent myContext={MyContext.Provider} />

Especially once the main Context itself is the Provider later on.

If you instead focus on encoding the provider type itself, e.g. as {$$typeof: REACT_PROVIDER_TYPE, name: "..."} or maybe [REACT_PROVIDER_TYPE, "..."] which then turns into ["$abc", "..."].

Then when this gets used in an element you automatically get the encoding ["$", ["$abc", "..."], "key", { children: ..., value: ... }].

You could also encode the whole context as an ID. That way it can be reused. See emitSymbolChunk for an example. So you can return the same instance quickly just by the ID if it occurs more than once in one response.

Then each instance would look more like ["$", "$def", "key", { children: ..., value: ... }].

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think <ClientComponent myContext={MyContext.Provider} /> would still work once we allow passing context like that because it wouldn't be the first element of this tuple right?

Copy link
Collaborator

@sebmarkbage sebmarkbage Mar 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you encode it? You could encode it as an object with {$$typeof: REACT_PROVIDER_TYPE, ...} but that's kind of long so instead of objects with $$typeof and named properties, I favored using arrays in the JSON format for known shapes since they're shorter. That's why elements are encoded this way.

So in that case I'd encode this as [REACT_PROVIDER_TYPE, ...] in which case it would be the first element of this tuple.

If you want to optimize this case by hardcoding a special value, it would make more sense to give it a different symbol for something like REACT_PROVIDER_ELEMENT_TYPE or something.

However, there's such a thing as over-specializing. You could easily just encode the Context Provider or Context Type and then have the element part be handled automatically. That way you support both cases with less code.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Interestingly the provider can be passed as a standalone thing with anything other than the name. However, if you pass the Context itself you would also have to pass the default value since now it can be consumed without being imported.)

return createElement(
getOrCreateServerContext((tuple[1]: any)).Provider,
tuple[2],
tuple[3],
);
}
return value;
}
Expand Down
231 changes: 231 additions & 0 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ let ReactNoopFlightServer;
let ReactNoopFlightClient;
let ErrorBoundary;
let NoErrorExpected;
let Scheduler;

describe('ReactFlight', () => {
beforeEach(() => {
Expand All @@ -27,6 +28,7 @@ describe('ReactFlight', () => {
ReactNoopFlightServer = require('react-noop-renderer/flight-server');
ReactNoopFlightClient = require('react-noop-renderer/flight-client');
act = require('jest-react').act;
Scheduler = require('scheduler');

ErrorBoundary = class extends React.Component {
state = {hasError: false, error: null};
Expand Down Expand Up @@ -302,4 +304,233 @@ describe('ReactFlight', () => {
{withoutStack: true},
);
});

describe('ServerContext', () => {
// @gate enableServerContext
it('supports basic createServerContext usage', () => {
const ServerContext = React.createServerContext(
'ServerContext',
'hello from server',
);
function Foo() {
const context = React.useServerContext(ServerContext);
return <div>{context}</div>;
}

const transport = ReactNoopFlightServer.render(<Foo />);
act(() => {
ServerContext._currentRenderer = null;
ServerContext._currentRenderer2 = null;
ReactNoop.render(ReactNoopFlightClient.read(transport));
});

expect(ReactNoop).toMatchRenderedOutput(<div>hello from server</div>);
});

// @gate enableServerContext
it('propagates ServerContext providers in flight', () => {
const ServerContext = React.createServerContext(
'ServerContext',
'default hello from server',
);

function Foo() {
return (
<div>
<ServerContext.Provider value="hi this is server">
<Bar />
</ServerContext.Provider>
</div>
);
}
function Bar() {
const context = React.useServerContext(ServerContext);
return context;
}

const transport = ReactNoopFlightServer.render(<Foo />);
act(() => {
ServerContext._currentRenderer = null;
ServerContext._currentRenderer2 = null;
ReactNoop.render(ReactNoopFlightClient.read(transport));
});

expect(ReactNoop).toMatchRenderedOutput(<div>hi this is server</div>);
});

// @gate enableServerContext
it('propagates ServerContext and cleansup providers in flight', () => {
const ServerContext = React.createServerContext(
'ServerContext',
'default hello from server',
);

function Foo() {
return (
<>
<ServerContext.Provider value="hi this is server outer">
<ServerContext.Provider value="hi this is server">
<Bar />
</ServerContext.Provider>
<ServerContext.Provider value="hi this is server2">
<Bar />
</ServerContext.Provider>
<Bar />
</ServerContext.Provider>
<ServerContext.Provider value="hi this is server outer2">
<Bar />
</ServerContext.Provider>
<Bar />
</>
);
}
function Bar() {
const context = React.useServerContext(ServerContext);
return <span>{context}</span>;
}

const transport = ReactNoopFlightServer.render(<Foo />);
act(() => {
ServerContext._currentRenderer = null;
ServerContext._currentRenderer2 = null;
ReactNoop.render(ReactNoopFlightClient.read(transport));
});

expect(ReactNoop).toMatchRenderedOutput(
<>
<span>hi this is server</span>
<span>hi this is server2</span>
<span>hi this is server outer</span>
<span>hi this is server outer2</span>
<span>default hello from server</span>
</>,
);
});

// @gate enableServerContext
it('propagates ServerContext providers in flight after suspending', async () => {
const ServerContext = React.createServerContext(
'ServerContext',
'default hello from server',
);

function Foo() {
return (
<div>
<ServerContext.Provider value="hi this is server">
<React.Suspense fallback={'Loading'}>
<Bar />
</React.Suspense>
</ServerContext.Provider>
</div>
);
}

let resolve;
const promise = new Promise(res => {
resolve = () => {
promise.unsuspend = true;
res();
};
});

function Bar() {
if (!promise.unsuspend) {
Scheduler.unstable_yieldValue('suspended');
throw promise;
}
Scheduler.unstable_yieldValue('rendered');
const context = React.useServerContext(ServerContext);
return context;
}

const transport = ReactNoopFlightServer.render(<Foo />);

expect(Scheduler).toHaveYielded(['suspended']);

await act(async () => {
resolve();
await promise;
jest.runAllImmediates();
});

expect(Scheduler).toHaveYielded(['rendered']);

act(() => {
ServerContext._currentRenderer = null;
ServerContext._currentRenderer2 = null;
ReactNoop.render(ReactNoopFlightClient.read(transport));
});

expect(ReactNoop).toMatchRenderedOutput(<div>hi this is server</div>);
});

// @gate enableServerContext
it('serializes ServerContext to client', async () => {
const ServerContext = React.createServerContext(
'ServerContext',
'default hello from server',
);

function ClientBar() {
Scheduler.unstable_yieldValue('ClientBar');
const context = React.useServerContext(ServerContext);
return <span>{context}</span>;
}

const Bar = moduleReference(ClientBar);

function Foo() {
return (
<ServerContext.Provider value="hi this is server">
<Bar />
</ServerContext.Provider>
);
}

const model = {
foo: <Foo />,
};

const transport = ReactNoopFlightServer.render(model);

expect(Scheduler).toHaveYielded([]);

act(() => {
ServerContext._currentRenderer = null;
ServerContext._currentRenderer2 = null;
const flightModel = ReactNoopFlightClient.read(transport);
ReactNoop.render(flightModel.foo);
});

expect(Scheduler).toHaveYielded(['ClientBar']);
expect(ReactNoop).toMatchRenderedOutput(<span>hi this is server</span>);
});

// @gate enableServerContext
it('takes ServerContext from client for refetching usecases', async () => {
const ServerContext = React.createServerContext(
'ServerContext',
'default hello from server',
);
function Bar() {
return <span>{React.useServerContext(ServerContext)}</span>;
}
const transport = ReactNoopFlightServer.render(<Bar />, {
context: [
{
name: 'ServerContext',
value: 'Override',
},
],
});

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

expect(ReactNoop).toMatchRenderedOutput(<span>Override</span>);
});
});
});
33 changes: 28 additions & 5 deletions packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import type {
MutableSourceGetSnapshotFn,
MutableSourceSubscribeFn,
ReactContext,
ReactServerContext,
ReactProviderType,
ServerContextJSONValue,
} from 'shared/ReactTypes';
import type {
Fiber,
Expand Down Expand Up @@ -103,7 +105,9 @@ function getCacheForType<T>(resourceType: () => T): T {
throw new Error('Not implemented.');
}

function readContext<T>(context: ReactContext<T>): T {
function readContext<T: any>(
context: ReactContext<T> | ReactServerContext<T>,
): T {
// For now we don't expose readContext usage in the hooks debugging info.
return context._currentValue;
}
Expand All @@ -117,6 +121,17 @@ function useContext<T>(context: ReactContext<T>): T {
return context._currentValue;
}

function useServerContext<T: ServerContextJSONValue>(
context: ReactServerContext<T>,
): T {
hookLog.push({
primitive: 'ServerContext',
stackError: new Error(),
value: context._currentValue,
});
return context._currentValue;
}

function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
Expand Down Expand Up @@ -331,7 +346,8 @@ function useId(): string {

const Dispatcher: DispatcherType = {
getCacheForType,
readContext,
// TODO: figure out why flow is complaining here
readContext: (readContext: any),
useCacheRefresh,
useCallback,
useContext,
Expand All @@ -343,6 +359,7 @@ const Dispatcher: DispatcherType = {
useMemo,
useReducer,
useRef,
useServerContext,
useState,
useTransition,
useMutableSource,
Expand Down Expand Up @@ -673,12 +690,16 @@ export function inspectHooks<Props>(
return buildTree(rootStack, readHookLog, includeHooksSource);
}

function setupContexts(contextMap: Map<ReactContext<any>, any>, fiber: Fiber) {
function setupContexts(
contextMap: Map<ReactContext<any> | ReactServerContext<any>, any>,
fiber: Fiber,
) {
let current = fiber;
while (current) {
if (current.tag === ContextProvider) {
const providerType: ReactProviderType<any> = current.type;
const context: ReactContext<any> = providerType._context;
const context: ReactContext<any> | ReactServerContext<any> =
providerType._context;
if (!contextMap.has(context)) {
// Store the current value that we're going to restore later.
contextMap.set(context, context._currentValue);
Expand All @@ -690,7 +711,9 @@ function setupContexts(contextMap: Map<ReactContext<any>, any>, fiber: Fiber) {
}
}

function restoreContexts(contextMap: Map<ReactContext<any>, any>) {
function restoreContexts(
contextMap: Map<ReactContext<any> | ReactServerContext<any>, any>,
) {
contextMap.forEach((value, context) => (context._currentValue = value));
}

Expand Down
Loading