Skip to content

Commit

Permalink
Context.set(newGlobalValue)
Browse files Browse the repository at this point in the history
Adds a method to update a context's default (global) value. This schedules
an update on all context consumers that are not nested inside a provider,
across all roots and renderers. For most use cases, this replaces the need
to inject a context provider at the top of each root.

(I've added an `unstable_` prefix until it's ready for public release.)
  • Loading branch information
acdlite committed Jul 27, 2018
1 parent 2a2ef7e commit 35bbf59
Show file tree
Hide file tree
Showing 14 changed files with 708 additions and 48 deletions.
9 changes: 8 additions & 1 deletion packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,14 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
},

renderLegacySyncRoot(element: React$Element<any>, callback: ?Function) {
const rootID = DEFAULT_ROOT_ID;
ReactNoop.renderLegacySyncRootWithID(element, DEFAULT_ROOT_ID, callback);
},

renderLegacySyncRootWithID(
element: React$Element<any>,
rootID: string,
callback: ?Function,
) {
const isAsync = false;
const container = ReactNoop.getOrCreateRootContainer(rootID, isAsync);
const root = roots.get(container.rootID);
Expand Down
23 changes: 19 additions & 4 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ import {
propagateContextChange,
readContext,
prepareToReadContext,
calculateChangedBits,
propagateRootContextChanges,
} from './ReactFiberNewContext';
import {
markActualRenderTimeStarted,
Expand All @@ -99,7 +99,8 @@ import {
resumeMountClassInstance,
updateClassInstance,
} from './ReactFiberClassComponent';
import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';
import MAX_SIGNED_31_BIT_INT from 'shared/maxSigned31BitInt';
import calculateChangedBits from 'shared/calculateChangedBits';

const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;

Expand Down Expand Up @@ -438,7 +439,7 @@ function updateHostRoot(current, workInProgress, renderExpirationTime) {
);
const nextProps = workInProgress.pendingProps;
const prevState = workInProgress.memoizedState;
const prevChildren = prevState !== null ? prevState.element : null;
const prevChildren = prevState.element;
processUpdateQueue(
workInProgress,
updateQueue,
Expand All @@ -447,6 +448,20 @@ function updateHostRoot(current, workInProgress, renderExpirationTime) {
renderExpirationTime,
);
const nextState = workInProgress.memoizedState;

let contextUpdate = nextState.firstContextUpdate;
if (contextUpdate !== null) {
propagateRootContextChanges(
workInProgress,
contextUpdate,
renderExpirationTime,
);
// After propagating the changes, we can clear the list immediately. This
// should work even with resuming because the updates are still in the
// update queue.
nextState.firstContextUpdate = nextState.lastContextUpdate = null;
}

// Caution: React DevTools currently depends on this property
// being called "element".
const nextChildren = nextState.element;
Expand Down Expand Up @@ -826,7 +841,7 @@ function updateContextProvider(current, workInProgress, renderExpirationTime) {
changedBits = MAX_SIGNED_31_BIT_INT;
} else {
const oldValue = oldProps.value;
changedBits = calculateChangedBits(context, newValue, oldValue);
changedBits = calculateChangedBits(context, oldValue, newValue);
if (changedBits === 0) {
// No change. Bailout early if children are the same.
if (
Expand Down
106 changes: 76 additions & 30 deletions packages/react-reconciler/src/ReactFiberNewContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,26 @@ export type ContextDependency<T> = {

import warningWithoutStack from 'shared/warningWithoutStack';
import {isPrimaryRenderer} from './ReactFiberHostConfig';
import {
scheduleWork,
computeExpirationForFiber,
requestCurrentTime,
} from './ReactFiberScheduler';
import {createCursor, push, pop} from './ReactFiberStack';
import maxSigned31BitInt from './maxSigned31BitInt';
import {NoWork} from './ReactFiberExpirationTime';
import {ContextProvider} from 'shared/ReactTypeOfWork';
import {enqueueUpdate, createUpdate} from './ReactUpdateQueue';

import invariant from 'shared/invariant';
import warning from 'shared/warning';

import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';
type RootContextUpdate<T> = {|
context: ReactContext<T>,
value: T,
changedBits: number,
next: RootContextUpdate<mixed> | null,
|};

const valueCursor: StackCursor<mixed> = createCursor(null);
const changedBitsCursor: StackCursor<number> = createCursor(0);

Expand Down Expand Up @@ -105,37 +116,71 @@ export function popProvider(providerFiber: Fiber): void {
}
}

export function calculateChangedBits<T>(
export function setRootContext<T>(
fiber: Fiber,
context: ReactContext<T>,
newValue: T,
oldValue: T,
value: T,
changedBits: number,
callback: (() => mixed) | null,
) {
// Use Object.is to compare the new context value to the old value. Inlined
// Object.is polyfill.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
if (
(oldValue === newValue &&
(oldValue !== 0 || 1 / oldValue === 1 / (newValue: any))) ||
(oldValue !== oldValue && newValue !== newValue) // eslint-disable-line no-self-compare
) {
// No change
return 0;
} else {
const changedBits =
typeof context._calculateChangedBits === 'function'
? context._calculateChangedBits(oldValue, newValue)
: MAX_SIGNED_31_BIT_INT;
// Schedule an update on the root.
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update = createUpdate(expirationTime);
update.payload = state => {
const contextUpdate = {
context,
value,
changedBits,
next: null,
};

if (__DEV__) {
warning(
(changedBits & MAX_SIGNED_31_BIT_INT) === changedBits,
'calculateChangedBits: Expected the return value to be a ' +
'31-bit integer. Instead received: %s',
changedBits,
);
// Append to the singly-linked list of pending context updates. The root
// will propagate this change to all matching context consumers that are not
// wrapped in a provider.
let firstContextUpdate = state.firstContextUpdate;
let lastContextUpdate = state.lastContextUpdate;
if (lastContextUpdate !== null) {
lastContextUpdate.next = contextUpdate;
} else {
firstContextUpdate = contextUpdate;
}
return changedBits | 0;
}
lastContextUpdate = contextUpdate;
return {
firstContextUpdate,
lastContextUpdate,
};
};
update.callback = callback;
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
}

export function propagateRootContextChanges(
workInProgress: Fiber,
firstContextUpdate: RootContextUpdate<mixed>,
renderExpirationTime: ExpirationTime,
): void {
let contextUpdate = firstContextUpdate;
do {
const context = contextUpdate.context;
const value = contextUpdate.value;
const changedBits = contextUpdate.changedBits;
if (isPrimaryRenderer) {
context._currentValue = value;
context._changedBits = changedBits;
} else {
context._currentValue2 = value;
context._changedBits2 = changedBits;
}
propagateContextChange(
workInProgress,
context,
changedBits,
renderExpirationTime,
);
contextUpdate = contextUpdate.next;
} while (contextUpdate !== null);
}

export function propagateContextChange(
Expand Down Expand Up @@ -217,7 +262,8 @@ export function propagateContextChange(
} while (dependency !== null);
} else if (fiber.tag === ContextProvider) {
// Don't scan deeper if this is a matching provider
nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
const providerContext: ReactContext<mixed> = fiber.type._context;
nextFiber = providerContext === context ? null : fiber.child;
} else {
// Traverse down.
nextFiber = fiber.child;
Expand Down
40 changes: 39 additions & 1 deletion packages/react-reconciler/src/ReactFiberReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
import type {ReactNodeList} from 'shared/ReactTypes';
import type {ExpirationTime} from './ReactFiberExpirationTime';

import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
findCurrentHostFiber,
findCurrentHostFiberWithNoPortals,
Expand Down Expand Up @@ -57,6 +58,8 @@ import {createUpdate, enqueueUpdate} from './ReactUpdateQueue';
import ReactFiberInstrumentation from './ReactFiberInstrumentation';
import * as ReactCurrentFiber from './ReactCurrentFiber';

const {ReactRootList} = ReactSharedInternals;

type OpaqueRoot = FiberRoot;

// 0 is PROD, 1 is DEV.
Expand Down Expand Up @@ -141,6 +144,22 @@ function scheduleRootUpdate(
return expirationTime;
}

function unmountRootFromGlobalList(root) {
// This root is no longer mounted. Remove it from the global list.
const previous = root.previousGlobalRoot;
const next = root.nextGlobalRoot;
if (previous !== null) {
previous.nextGlobalRoot = next;
} else {
ReactRootList.first = next;
}
if (next !== null) {
next.previousGlobalRoot = previous;
} else {
ReactRootList.last = previous;
}
}

export function updateContainerAtExpirationTime(
element: ReactNodeList,
container: OpaqueRoot,
Expand All @@ -163,14 +182,33 @@ export function updateContainerAtExpirationTime(
}
}

let wrappedCallback;
if (element === null) {
// Assume this is an unmount and mark the root for clean-up from the
// global list.
// TODO: Add an explicit API for unmounting to the reconciler API, instead
// of inferring based on the children.
if (callback !== null && callback !== undefined) {
const cb = callback;
wrappedCallback = function() {
unmountRootFromGlobalList(container);
return cb.call(this);
};
} else {
wrappedCallback = unmountRootFromGlobalList.bind(null, container);
}
} else {
wrappedCallback = callback;
}

const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}

return scheduleRootUpdate(current, element, expirationTime, callback);
return scheduleRootUpdate(current, element, expirationTime, wrappedCallback);
}

function findHostInstance(component: Object): PublicInstance | null {
Expand Down
37 changes: 37 additions & 0 deletions packages/react-reconciler/src/ReactFiberRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@
* @flow
*/

import type {ReactContext} from 'shared/ReactTypes';
import type {Fiber} from './ReactFiber';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';

import ReactSharedInternals from 'shared/ReactSharedInternals';
import {noTimeout} from './ReactFiberHostConfig';

import {createHostRootFiber} from './ReactFiber';
import {NoWork} from './ReactFiberExpirationTime';
import {setRootContext} from './ReactFiberNewContext';

const {ReactRootList} = ReactSharedInternals;

// TODO: This should be lifted into the renderer.
export type Batch = {
Expand Down Expand Up @@ -73,13 +78,27 @@ export type FiberRoot = {
firstBatch: Batch | null,
// Linked-list of roots
nextScheduledRoot: FiberRoot | null,

// Linked-list of global roots. This is cross-renderer.
nextGlobalRoot: FiberRoot | null,
previousGlobalRoot: FiberRoot | null,

// Schedules a context update.
setContext<T>(
context: ReactContext<T>,
value: T,
changedBits: number,
callback: (() => mixed) | null,
): void,
};

export function createFiberRoot(
containerInfo: any,
isAsync: boolean,
hydrate: boolean,
): FiberRoot {
const lastGlobalRoot = ReactRootList.last;

// Cyclic construction. This cheats the type system right now because
// stateNode is any.
const uninitializedFiber = createHostRootFiber(isAsync);
Expand All @@ -106,7 +125,25 @@ export function createFiberRoot(
expirationTime: NoWork,
firstBatch: null,
nextScheduledRoot: null,

nextGlobalRoot: null,
previousGlobalRoot: lastGlobalRoot,
setContext: (setRootContext.bind(null, uninitializedFiber): any),
};
uninitializedFiber.stateNode = root;
uninitializedFiber.memoizedState = {
element: null,
firstContextUpdate: null,
lastContextUpdate: null,
};

// Append to the global list of roots
if (lastGlobalRoot === null) {
ReactRootList.first = root;
} else {
lastGlobalRoot.nextGlobalRoot = root;
}
ReactRootList.last = root;

return root;
}
Loading

0 comments on commit 35bbf59

Please sign in to comment.