Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fizz] Implement New Context #21255

Merged
merged 9 commits into from
Apr 14, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
148 changes: 148 additions & 0 deletions packages/react-server/src/ReactFizzNewContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import type {ReactContext} from 'shared/ReactTypes';

import invariant from 'shared/invariant';

// TODO: Move to format config.
const isPrimaryRenderer = true;

let rendererSigil;
if (__DEV__) {
// Use this to detect multiple renderers using the same context
rendererSigil = {};
}

// Used to store the parent path of all context overrides in a shared linked list.
// Forming a reverse tree.
type ContextNode<T> = {
parent: null | ContextNode<any>,
depth: number, // Short hand to compute the depth of the tree at this node.
context: ReactContext<T>,
parentValue: T,
value: T,
};

// The structure of a context snapshot is an implementation of this file.
// Currently, it's implemented as tracking the current active node.
export opaque type ContextSnapshot = null | ContextNode<any>;

export const rootContextSnapshot: ContextSnapshot = null;

// We assume that this runtime owns the "current" field on all ReactContext instances.
// This global (actually thread local) state represents what state all those "current",
// fields are currently in.
let currentActiveSnapshot: ContextSnapshot = null;

// Perform context switching to the new snapshot.
export function switchContext(newSnapshot: ContextSnapshot): void {
// TODO: Switch the context.
}

export function pushProvider<T>(
context: ReactContext<T>,
nextValue: T,
): ContextSnapshot {
let prevValue;
if (isPrimaryRenderer) {
prevValue = context._currentValue;
context._currentValue = nextValue;
if (__DEV__) {
if (
context._currentRenderer !== undefined &&
context._currentRenderer !== null &&
context._currentRenderer !== rendererSigil
) {
console.error(
'Detected multiple renderers concurrently rendering the ' +
'same context provider. This is currently unsupported.',
);
}
context._currentRenderer = rendererSigil;
}
} else {
prevValue = context._currentValue2;
context._currentValue2 = nextValue;
if (__DEV__) {
if (
context._currentRenderer2 !== undefined &&
context._currentRenderer2 !== null &&
context._currentRenderer2 !== rendererSigil
) {
console.error(
'Detected multiple renderers concurrently rendering the ' +
'same context provider. This is currently unsupported.',
);
}
context._currentRenderer2 = rendererSigil;
}
}
const prevNode = currentActiveSnapshot;
const newNode: ContextNode<T> = {
parent: prevNode,
depth: prevNode === null ? 0 : prevNode.depth + 1,
context: context,
parentValue: prevValue,
value: nextValue,
};
currentActiveSnapshot = newNode;
return newNode;
}

export function popProvider<T>(context: ReactContext<T>): void {
const prevSnapshot = currentActiveSnapshot;
invariant(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why check this in prod?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It follows the principle that if it's getting checked by runtime type checks here anyway, it likely isn't a perf cost (other than the extra code). The prevSnapshot.context access after does that anyway.

prevSnapshot !== null,
'Tried to pop a Context at the root of the app. This is a bug in React.',
);
if (__DEV__) {
if (prevSnapshot.context !== context) {
console.error(
'The parent context is not the expected context. This is probably a bug in React.',
);
}
}
if (isPrimaryRenderer) {
prevSnapshot.context._currentValue = prevSnapshot.parentValue;
if (__DEV__) {
if (
context._currentRenderer !== undefined &&
context._currentRenderer !== null &&
context._currentRenderer !== rendererSigil
) {
console.error(
'Detected multiple renderers concurrently rendering the ' +
'same context provider. This is currently unsupported.',
);
}
context._currentRenderer = rendererSigil;
}
} else {
prevSnapshot.context._currentValue2 = prevSnapshot.parentValue;
if (__DEV__) {
if (
context._currentRenderer2 !== undefined &&
context._currentRenderer2 !== null &&
context._currentRenderer2 !== rendererSigil
) {
console.error(
'Detected multiple renderers concurrently rendering the ' +
'same context provider. This is currently unsupported.',
);
}
context._currentRenderer2 = rendererSigil;
}
}
currentActiveSnapshot = prevSnapshot.parent;
}

export function getActiveContext(): ContextSnapshot {
return currentActiveSnapshot;
}
32 changes: 32 additions & 0 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
ResponseState,
FormatContext,
} from './ReactServerFormatConfig';
import type {ContextSnapshot} from './ReactFizzNewContext';

import {
scheduleWork,
Expand Down Expand Up @@ -56,6 +57,12 @@ import {
processChildContext,
emptyContextObject,
} from './ReactFizzContext';
import {
rootContextSnapshot,
switchContext,
getActiveContext,
} from './ReactFizzNewContext';

import {
getIteratorFn,
REACT_ELEMENT_TYPE,
Expand Down Expand Up @@ -104,6 +111,7 @@ type Task = {
blockedSegment: Segment, // the segment we'll write to
abortSet: Set<Task>, // the abortable set that this task belongs to
legacyContext: LegacyContext, // the current legacy context that this task is executing in
context: ContextSnapshot, // the current new context that this task is executing in
assignID: null | SuspenseBoundaryID, // id to assign to the content
};

Expand Down Expand Up @@ -220,6 +228,7 @@ export function createRequest(
rootSegment,
abortSet,
emptyContextObject,
rootContextSnapshot,
null,
);
pingedTasks.push(rootTask);
Expand Down Expand Up @@ -257,6 +266,7 @@ function createTask(
blockedSegment: Segment,
abortSet: Set<Task>,
legacyContext: LegacyContext,
context: ContextSnapshot,
assignID: null | SuspenseBoundaryID,
): Task {
request.allPendingTasks++;
Expand All @@ -272,6 +282,7 @@ function createTask(
blockedSegment,
abortSet,
legacyContext,
context,
assignID,
};
abortSet.add(task);
Expand Down Expand Up @@ -394,6 +405,7 @@ function renderSuspenseBoundary(
boundarySegment,
fallbackAbortSet,
task.legacyContext,
task.context,
newBoundary.id, // This is the ID we want to give this fallback so we can replace it later.
);
// TODO: This should be queued at a separate lower priority queue so that we only work
Expand Down Expand Up @@ -918,6 +930,7 @@ function spawnNewSuspendedTask(
newSegment,
task.abortSet,
task.legacyContext,
task.context,
task.assignID,
);
// We've delegated the assignment.
Expand All @@ -936,6 +949,7 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
// process.
const previousFormatContext = task.blockedSegment.formatContext;
const previousLegacyContext = task.legacyContext;
const previousContext = task.context;
try {
return renderNodeDestructive(request, task, node);
} catch (x) {
Expand All @@ -945,6 +959,9 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
// functions in case nothing throws so we don't use "finally" here.
task.blockedSegment.formatContext = previousFormatContext;
task.legacyContext = previousLegacyContext;
task.context = previousContext;
// Restore all active ReactContexts to what they were before.
switchContext(previousContext);
} else {
// We assume that we don't need the correct context.
// Let's terminate the rest of the tree and don't render any siblings.
Expand Down Expand Up @@ -1104,6 +1121,10 @@ function retryTask(request: Request, task: Task): void {
// We completed this by other means before we had a chance to retry it.
return;
}
// We restore the context to what it was when we suspended.
// We don't restore it after we leave because it's likely that we'll end up
// needing a very similar context soon again.
switchContext(task.context);
try {
// We call the destructive form that mutates this task. That way if something
// suspends again, we can reuse the same task instead of spawning a new one.
Expand All @@ -1129,6 +1150,7 @@ function performWork(request: Request): void {
if (request.status === CLOSED) {
return;
}
const prevContext = getActiveContext();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = Dispatcher;

Expand All @@ -1148,6 +1170,16 @@ function performWork(request: Request): void {
fatalError(request, error);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
if (prevDispatcher === Dispatcher) {
Copy link
Collaborator Author

@sebmarkbage sebmarkbage Apr 13, 2021

Choose a reason for hiding this comment

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

This case is meant to handle something like:

function Foo() {
  renderToString(<Bar />);
}
renderToString(<Foo />);

In a follow up.

// This means that we were in a reentrant work loop. This could happen
// in a renderer that supports synchronous work like renderToString,
// when it's called from within another renderer.
// Normally we don't bother switching the contexts to their root/default
// values when leaving because we'll likely need the same or similar
// context again. However, when we're inside a synchronous loop like this
// we'll to restore the context to what it was before returning.
switchContext(prevContext);
}
}
}

Expand Down