Skip to content

Commit

Permalink
Add ReactDOM.unstable_createSyncRoot (#15504)
Browse files Browse the repository at this point in the history
* Add ReactDOM.unstable_createSyncRoot

- `ReactDOM.unstable_createRoot` creates a Concurrent Mode root.
- `ReactDOM.unstable_createSyncRoot` creates a Batched Mode root. It
does not support `createBatch`.
- `ReactDOM.render` creates a Legacy Mode root. It will eventually be
deprecated and possibly moved to a separate entry point, like
`react-dom/legacy`.

* Rename internal ReactRoot types
  • Loading branch information
acdlite committed May 13, 2019
1 parent 862f499 commit 283ce53
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 119 deletions.
Expand Up @@ -661,6 +661,37 @@ describe('ReactDOMFiberAsync', () => {
});
});

describe('createSyncRoot', () => {
it('updates flush without yielding in the next event', () => {
const root = ReactDOM.unstable_createSyncRoot(container);

function Text(props) {
Scheduler.yieldValue(props.text);
return props.text;
}

root.render(
<React.Fragment>
<Text text="A" />
<Text text="B" />
<Text text="C" />
</React.Fragment>,
);

// Nothing should have rendered yet
expect(container.textContent).toEqual('');

// Everything should render immediately in the next event
expect(Scheduler).toFlushExpired(['A', 'B', 'C']);
expect(container.textContent).toEqual('ABC');
});

it('does not support createBatch', () => {
const root = ReactDOM.unstable_createSyncRoot(container);
expect(root.createBatch).toBe(undefined);
});
});

describe('Disable yielding', () => {
beforeEach(() => {
jest.resetModules();
Expand Down
130 changes: 71 additions & 59 deletions packages/react-dom/src/client/ReactDOM.js
Expand Up @@ -53,7 +53,7 @@ import {
accumulateTwoPhaseDispatches,
accumulateDirectDispatches,
} from 'events/EventPropagators';
import {LegacyRoot, ConcurrentRoot} from 'shared/ReactRootTags';
import {LegacyRoot, ConcurrentRoot, BatchedRoot} from 'shared/ReactRootTags';
import {has as hasInstance} from 'shared/ReactInstanceMap';
import ReactVersion from 'shared/ReactVersion';
import ReactSharedInternals from 'shared/ReactSharedInternals';
Expand Down Expand Up @@ -159,11 +159,11 @@ setRestoreImplementation(restoreControlledState);

export type DOMContainer =
| (Element & {
_reactRootContainer: ?Root,
_reactRootContainer: ?(_ReactRoot | _ReactSyncRoot),
_reactHasBeenPassedToCreateRootDEV: ?boolean,
})
| (Document & {
_reactRootContainer: ?Root,
_reactRootContainer: ?(_ReactRoot | _ReactSyncRoot),
_reactHasBeenPassedToCreateRootDEV: ?boolean,
});

Expand All @@ -175,28 +175,26 @@ type Batch = FiberRootBatch & {
// The ReactRoot constructor is hoisted but the prototype methods are not. If
// we move ReactRoot to be above ReactBatch, the inverse error occurs.
// $FlowFixMe Hoisting issue.
_root: Root,
_root: _ReactRoot | _ReactSyncRoot,
_hasChildren: boolean,
_children: ReactNodeList,

_callbacks: Array<() => mixed> | null,
_didComplete: boolean,
};

type Root = {
type _ReactSyncRoot = {
render(children: ReactNodeList, callback: ?() => mixed): Work,
unmount(callback: ?() => mixed): Work,
legacy_renderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
callback: ?() => mixed,
): Work,
createBatch(): Batch,

_internalRoot: FiberRoot,
};

function ReactBatch(root: ReactRoot) {
type _ReactRoot = _ReactSyncRoot & {
createBatch(): Batch,
};

function ReactBatch(root: _ReactRoot | _ReactSyncRoot) {
const expirationTime = computeUniqueAsyncExpiration();
this._expirationTime = expirationTime;
this._root = root;
Expand Down Expand Up @@ -363,11 +361,22 @@ ReactWork.prototype._onCommit = function(): void {
}
};

function ReactRoot(container: DOMContainer, tag: RootTag, hydrate: boolean) {
function ReactSyncRoot(
container: DOMContainer,
tag: RootTag,
hydrate: boolean,
) {
// Tag is either LegacyRoot or Concurrent Root
const root = createContainer(container, tag, hydrate);
this._internalRoot = root;
}
ReactRoot.prototype.render = function(

function ReactRoot(container: DOMContainer, hydrate: boolean) {
const root = createContainer(container, ConcurrentRoot, hydrate);
this._internalRoot = root;
}

ReactRoot.prototype.render = ReactSyncRoot.prototype.render = function(
children: ReactNodeList,
callback: ?() => mixed,
): Work {
Expand All @@ -383,22 +392,8 @@ ReactRoot.prototype.render = function(
updateContainer(children, root, null, work._onCommit);
return work;
};
ReactRoot.prototype.unmount = function(callback: ?() => mixed): Work {
const root = this._internalRoot;
const work = new ReactWork();
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'render');
}
if (callback !== null) {
work.then(callback);
}
updateContainer(null, root, null, work._onCommit);
return work;
};
ReactRoot.prototype.legacy_renderSubtreeIntoContainer = function(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,

ReactRoot.prototype.unmount = ReactSyncRoot.prototype.unmount = function(
callback: ?() => mixed,
): Work {
const root = this._internalRoot;
Expand All @@ -410,9 +405,11 @@ ReactRoot.prototype.legacy_renderSubtreeIntoContainer = function(
if (callback !== null) {
work.then(callback);
}
updateContainer(children, root, parentComponent, work._onCommit);
updateContainer(null, root, null, work._onCommit);
return work;
};

// Sync roots cannot create batches. Only concurrent ones.
ReactRoot.prototype.createBatch = function(): Batch {
const batch = new ReactBatch(this);
const expirationTime = batch._expirationTime;
Expand Down Expand Up @@ -492,7 +489,7 @@ let warnedAboutHydrateAPI = false;
function legacyCreateRootFromDOMContainer(
container: DOMContainer,
forceHydrate: boolean,
): Root {
): _ReactSyncRoot {
const shouldHydrate =
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
// First clear any existing content.
Expand Down Expand Up @@ -529,7 +526,9 @@ function legacyCreateRootFromDOMContainer(
);
}
}
return new ReactRoot(container, LegacyRoot, shouldHydrate);

// Legacy roots are not batched.
return new ReactSyncRoot(container, LegacyRoot, shouldHydrate);
}

function legacyRenderSubtreeIntoContainer(
Expand All @@ -541,56 +540,44 @@ function legacyRenderSubtreeIntoContainer(
) {
if (__DEV__) {
topLevelUpdateWarnings(container);
warnOnInvalidCallback(callback === undefined ? null : callback, 'render');
}

// TODO: Without `any` type, Flow says "Property cannot be accessed on any
// member of intersection type." Whyyyyyy.
let root: Root = (container._reactRootContainer: any);
let root: _ReactSyncRoot = (container._reactRootContainer: any);
let fiberRoot;
if (!root) {
// Initial mount
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(root._internalRoot);
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Initial mount should not be batched.
unbatchedUpdates(() => {
if (parentComponent != null) {
root.legacy_renderSubtreeIntoContainer(
parentComponent,
children,
callback,
);
} else {
root.render(children, callback);
}
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(root._internalRoot);
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Update
if (parentComponent != null) {
root.legacy_renderSubtreeIntoContainer(
parentComponent,
children,
callback,
);
} else {
root.render(children, callback);
}
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(root._internalRoot);
return getPublicRootInstance(fiberRoot);
}

function createPortal(
Expand Down Expand Up @@ -800,6 +787,7 @@ const ReactDOM: Object = {
flushSync: flushSync,

unstable_createRoot: createRoot,
unstable_createSyncRoot: createSyncRoot,
unstable_flushControlled: flushControlled,

__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
Expand All @@ -826,7 +814,10 @@ type RootOptions = {
hydrate?: boolean,
};

function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
function createRoot(
container: DOMContainer,
options?: RootOptions,
): _ReactRoot {
const functionName = enableStableConcurrentModeAPIs
? 'createRoot'
: 'unstable_createRoot';
Expand All @@ -835,6 +826,29 @@ function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
'%s(...): Target container is not a DOM element.',
functionName,
);
warnIfReactDOMContainerInDEV(container);
const hydrate = options != null && options.hydrate === true;
return new ReactRoot(container, hydrate);
}

function createSyncRoot(
container: DOMContainer,
options?: RootOptions,
): _ReactSyncRoot {
const functionName = enableStableConcurrentModeAPIs
? 'createRoot'
: 'unstable_createRoot';
invariant(
isValidContainer(container),
'%s(...): Target container is not a DOM element.',
functionName,
);
warnIfReactDOMContainerInDEV(container);
const hydrate = options != null && options.hydrate === true;
return new ReactSyncRoot(container, BatchedRoot, hydrate);
}

function warnIfReactDOMContainerInDEV(container) {
if (__DEV__) {
warningWithoutStack(
!container._reactRootContainer,
Expand All @@ -844,13 +858,11 @@ function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
);
container._reactHasBeenPassedToCreateRootDEV = true;
}
const hydrate = options != null && options.hydrate === true;
return new ReactRoot(container, ConcurrentRoot, hydrate);
}

if (enableStableConcurrentModeAPIs) {
ReactDOM.createRoot = createRoot;
ReactDOM.unstable_createRoot = undefined;
ReactDOM.createSyncRoot = createSyncRoot;
}

const foundDevTools = injectIntoDevTools({
Expand Down

0 comments on commit 283ce53

Please sign in to comment.