2017-05-25 00:06:30 +08:00
|
|
|
/**
|
2018-09-08 06:11:23 +08:00
|
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
2017-05-25 00:06:30 +08:00
|
|
|
*
|
2017-09-25 04:48:13 +08:00
|
|
|
* This source code is licensed under the MIT license found in the
|
|
|
|
|
* LICENSE file in the root directory of this source tree.
|
2017-05-25 00:06:30 +08:00
|
|
|
*
|
|
|
|
|
* @flow
|
|
|
|
|
*/
|
|
|
|
|
|
2020-03-31 03:42:41 +08:00
|
|
|
import type {TouchedViewDataAtPoint} from './ReactNativeTypes';
|
|
|
|
|
|
2017-10-26 02:07:54 +08:00
|
|
|
// Modules provided by RN:
|
2019-05-23 15:23:54 +08:00
|
|
|
import {
|
|
|
|
|
ReactNativeViewConfigRegistry,
|
|
|
|
|
UIManager,
|
|
|
|
|
deepFreezeAndThrowOnMutationInDev,
|
|
|
|
|
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
|
2017-11-03 03:50:03 +08:00
|
|
|
|
2018-11-20 07:32:54 +08:00
|
|
|
import {create, diff} from './ReactNativeAttributePayload';
|
2017-11-05 19:58:36 +08:00
|
|
|
import {
|
2017-10-26 02:07:54 +08:00
|
|
|
precacheFiberNode,
|
|
|
|
|
uncacheFiberNode,
|
|
|
|
|
updateFiberProps,
|
2017-11-05 19:58:36 +08:00
|
|
|
} from './ReactNativeComponentTree';
|
|
|
|
|
import ReactNativeFiberHostComponent from './ReactNativeFiberHostComponent';
|
2017-05-25 00:06:30 +08:00
|
|
|
|
2021-03-24 04:57:28 +08:00
|
|
|
import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities';
|
2021-02-10 02:32:20 +08:00
|
|
|
|
2019-05-23 15:23:54 +08:00
|
|
|
const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
|
|
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
export type Type = string;
|
|
|
|
|
export type Props = Object;
|
|
|
|
|
export type Container = number;
|
2020-02-26 05:54:27 +08:00
|
|
|
export type Instance = ReactNativeFiberHostComponent;
|
2018-05-19 18:29:11 +08:00
|
|
|
export type TextInstance = number;
|
|
|
|
|
export type HydratableInstance = Instance | TextInstance;
|
|
|
|
|
export type PublicInstance = Instance;
|
|
|
|
|
export type HostContext = $ReadOnly<{|
|
2018-05-15 06:34:01 +08:00
|
|
|
isInAParentText: boolean,
|
|
|
|
|
|}>;
|
2018-05-19 18:29:11 +08:00
|
|
|
export type UpdatePayload = Object; // Unused
|
|
|
|
|
export type ChildSet = void; // Unused
|
2018-05-15 06:34:01 +08:00
|
|
|
|
2018-07-04 10:22:41 +08:00
|
|
|
export type TimeoutHandle = TimeoutID;
|
|
|
|
|
export type NoTimeout = -1;
|
|
|
|
|
|
2020-03-31 03:42:41 +08:00
|
|
|
export type RendererInspectionConfig = $ReadOnly<{|
|
|
|
|
|
// Deprecated. Replaced with getInspectorDataForViewAtPoint.
|
|
|
|
|
getInspectorDataForViewTag?: (tag: number) => Object,
|
|
|
|
|
getInspectorDataForViewAtPoint?: (
|
|
|
|
|
inspectedView: Object,
|
|
|
|
|
locationX: number,
|
|
|
|
|
locationY: number,
|
|
|
|
|
callback: (viewData: TouchedViewDataAtPoint) => mixed,
|
|
|
|
|
) => void,
|
|
|
|
|
|}>;
|
|
|
|
|
|
Inline fbjs/lib/emptyObject (#13055)
* Inline fbjs/lib/emptyObject
* Explicit naming
* Compare to undefined
* Another approach for detecting whether we can mutate
Each renderer would have its own local LegacyRefsObject function.
While in general we don't want `instanceof`, here it lets us do a simple check: did *we* create the refs object?
Then we can mutate it.
If the check didn't pass, either we're attaching ref for the first time (so we know to use the constructor),
or (unlikely) we're attaching a ref to a component owned by another renderer. In this case, to avoid "losing"
refs, we assign them onto the new object. Even in that case it shouldn't "hop" between renderers anymore.
* Clearer naming
* Add test case for strings refs across renderers
* Use a shared empty object for refs by reading it from React
* Remove string refs from ReactART test
It's not currently possible to resetModules() between several renderers
without also resetting the `React` module. However, that leads to losing
the referential identity of the empty ref object, and thus subsequent
checks in the renderers for whether it is pooled fail (and cause assignments
to a frozen object).
This has always been the case, but we used to work around it by shimming
fbjs/lib/emptyObject in tests and preserving its referential identity.
This won't work anymore because we've inlined it. And preserving referential
identity of React itself wouldn't be great because it could be confusing during
testing (although we might want to revisit this in the future by moving its
stateful parts into a separate package).
For now, I'm removing string ref usage from this test because only this is
the only place in our tests where we hit this problem, and it's only
related to string refs, and not just ref mechanism in general.
* Simplify the condition
2018-06-19 20:41:42 +08:00
|
|
|
const UPDATE_SIGNAL = {};
|
|
|
|
|
if (__DEV__) {
|
|
|
|
|
Object.freeze(UPDATE_SIGNAL);
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-10 09:41:13 +08:00
|
|
|
// Counter for uniquely identifying views.
|
|
|
|
|
// % 10 === 1 means it is a rootTag.
|
|
|
|
|
// % 2 === 0 means it is a Fabric tag.
|
|
|
|
|
let nextReactTag = 3;
|
|
|
|
|
function allocateTag() {
|
|
|
|
|
let tag = nextReactTag;
|
|
|
|
|
if (tag % 10 === 1) {
|
|
|
|
|
tag += 2;
|
|
|
|
|
}
|
|
|
|
|
nextReactTag = tag + 2;
|
|
|
|
|
return tag;
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-25 00:06:30 +08:00
|
|
|
function recursivelyUncacheFiberNode(node: Instance | TextInstance) {
|
|
|
|
|
if (typeof node === 'number') {
|
|
|
|
|
// Leaf node (eg text)
|
|
|
|
|
uncacheFiberNode(node);
|
|
|
|
|
} else {
|
|
|
|
|
uncacheFiberNode((node: any)._nativeTag);
|
|
|
|
|
|
|
|
|
|
(node: any)._children.forEach(recursivelyUncacheFiberNode);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-22 06:22:01 +08:00
|
|
|
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoPersistence';
|
|
|
|
|
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration';
|
2020-05-02 02:01:43 +08:00
|
|
|
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
|
2020-05-06 01:37:46 +08:00
|
|
|
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
|
Add `supportsMicrotasks` to the host config (#20809)
* Add `supportsMicrotasks` to the host config
Only certain renderers support scheduling a microtask, so we need a
renderer specific flag that we can toggle. That way it's off for some
renderers and on for others.
I copied the approach we use for the other optional parts of the host
config, like persistent mode and test selectors.
Why isn't the feature flag sufficient?
The feature flag modules, confusingly, are not renderer-specific, at
least when running the our tests against the source files. They are
meant to correspond to a release channel, not a renderer, but we got
confused at some point and haven't cleaned it up.
For example, when we run `yarn test`, Jest loads the flags from the
default `ReactFeatureFlags.js` module, even when we import the React
Native renderer — but in the actual builds, we load a different feature
flag module, `ReactFeatureFlags.native-oss.js.` There's no way in our
current Jest load a different host config for each renderer, because
they all just import the same module. We should solve this by creating
separate Jest project for each renderer, so that the flags loaded when
running against source are the same ones that we use in the
compiled bundles.
The feature flag (`enableDiscreteMicrotasks`) still exists — it's used
to set the React DOM host config's `supportsMicrotasks` flag to `true`.
(Same for React Noop) The important part is that turning on the feature
flag does *not* affect the other renderers, like React Native.
The host config will likely outlive the feature flag, too, since the
feature flag only exists so we can gradually roll it out and measure the
impact in production; once we do, we'll remove it. Whereas the host
config flag may continue to be used to disable the discrete microtask
behavior for RN, because RN will likely use a native (non-JavaScript)
API to schedule its tasks.
* Add `supportsMicrotask` to react-reconciler README
2021-02-13 05:13:49 +08:00
|
|
|
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
|
2018-05-19 18:29:11 +08:00
|
|
|
|
|
|
|
|
export function appendInitialChild(
|
|
|
|
|
parentInstance: Instance,
|
|
|
|
|
child: Instance | TextInstance,
|
|
|
|
|
): void {
|
|
|
|
|
parentInstance._children.push(child);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createInstance(
|
|
|
|
|
type: string,
|
|
|
|
|
props: Props,
|
|
|
|
|
rootContainerInstance: Container,
|
|
|
|
|
hostContext: HostContext,
|
|
|
|
|
internalInstanceHandle: Object,
|
|
|
|
|
): Instance {
|
|
|
|
|
const tag = allocateTag();
|
2018-11-20 07:32:54 +08:00
|
|
|
const viewConfig = getViewConfigForType(type);
|
2018-05-19 18:29:11 +08:00
|
|
|
|
|
|
|
|
if (__DEV__) {
|
|
|
|
|
for (const key in viewConfig.validAttributes) {
|
|
|
|
|
if (props.hasOwnProperty(key)) {
|
|
|
|
|
deepFreezeAndThrowOnMutationInDev(props[key]);
|
2017-05-25 00:06:30 +08:00
|
|
|
}
|
|
|
|
|
}
|
2018-05-19 18:29:11 +08:00
|
|
|
}
|
2017-05-25 00:06:30 +08:00
|
|
|
|
2018-11-20 07:32:54 +08:00
|
|
|
const updatePayload = create(props, viewConfig.validAttributes);
|
2017-05-25 00:06:30 +08:00
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
UIManager.createView(
|
|
|
|
|
tag, // reactTag
|
|
|
|
|
viewConfig.uiViewClassName, // viewName
|
|
|
|
|
rootContainerInstance, // rootTag
|
|
|
|
|
updatePayload, // props
|
|
|
|
|
);
|
2017-05-25 00:06:30 +08:00
|
|
|
|
2020-03-31 03:42:41 +08:00
|
|
|
const component = new ReactNativeFiberHostComponent(
|
|
|
|
|
tag,
|
|
|
|
|
viewConfig,
|
|
|
|
|
internalInstanceHandle,
|
|
|
|
|
);
|
2018-05-15 06:34:01 +08:00
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
precacheFiberNode(internalInstanceHandle, tag);
|
|
|
|
|
updateFiberProps(tag, props);
|
2017-05-25 00:06:30 +08:00
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
// Not sure how to avoid this cast. Flow is okay if the component is defined
|
|
|
|
|
// in the same file but if it's external it can't see the types.
|
|
|
|
|
return ((component: any): Instance);
|
|
|
|
|
}
|
2017-05-25 00:06:30 +08:00
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
export function createTextInstance(
|
|
|
|
|
text: string,
|
|
|
|
|
rootContainerInstance: Container,
|
|
|
|
|
hostContext: HostContext,
|
|
|
|
|
internalInstanceHandle: Object,
|
|
|
|
|
): TextInstance {
|
[RFC] Codemod invariant -> throw new Error (#22435)
* Hoist error codes import to module scope
When this code was written, the error codes map (`codes.json`) was
created on-the-fly, so we had to lazily require from inside the visitor.
Because `codes.json` is now checked into source, we can import it a
single time in module scope.
* Minify error constructors in production
We use a script to minify our error messages in production. Each message
is assigned an error code, defined in `scripts/error-codes/codes.json`.
Then our build script replaces the messages with a link to our
error decoder page, e.g. https://reactjs.org/docs/error-decoder.html/?invariant=92
This enables us to write helpful error messages without increasing the
bundle size.
Right now, the script only works for `invariant` calls. It does not work
if you throw an Error object. This is an old Facebookism that we don't
really need, other than the fact that our error minification script
relies on it.
So, I've updated the script to minify error constructors, too:
Input:
Error(`A ${adj} message that contains ${noun}`);
Output:
Error(formatProdErrorMessage(ERR_CODE, adj, noun));
It only works for constructors that are literally named Error, though we
could add support for other names, too.
As a next step, I will add a lint rule to enforce that errors written
this way must have a corresponding error code.
* Minify "no fallback UI specified" error in prod
This error message wasn't being minified because it doesn't use
invariant. The reason it didn't use invariant is because this particular
error is created without begin thrown — it doesn't need to be thrown
because it's located inside the error handling part of the runtime.
Now that the error minification script supports Error constructors, we
can minify it by assigning it a production error code in
`scripts/error-codes/codes.json`.
To support the use of Error constructors more generally, I will add a
lint rule that enforces each message has a corresponding error code.
* Lint rule to detect unminified errors
Adds a lint rule that detects when an Error constructor is used without
a corresponding production error code.
We already have this for `invariant`, but not for regular errors, i.e.
`throw new Error(msg)`. There's also nothing that enforces the use of
`invariant` besides convention.
There are some packages where we don't care to minify errors. These are
packages that run in environments where bundle size is not a concern,
like react-pg. I added an override in the ESLint config to ignore these.
* Temporarily add invariant codemod script
I'm adding this codemod to the repo temporarily, but I'll revert it
in the same PR. That way we don't have to check it in but it's still
accessible (via the PR) if we need it later.
* [Automated] Codemod invariant -> Error
This commit contains only automated changes:
npx jscodeshift -t scripts/codemod-invariant.js packages --ignore-pattern="node_modules/**/*"
yarn linc --fix
yarn prettier
I will do any manual touch ups in separate commits so they're easier
to review.
* Remove temporary codemod script
This reverts the codemod script and ESLint config I added temporarily
in order to perform the invariant codemod.
* Manual touch ups
A few manual changes I made after the codemod ran.
* Enable error code transform per package
Currently we're not consistent about which packages should have their
errors minified in production and which ones should.
This adds a field to the bundle configuration to control whether to
apply the transform. We should decide what the criteria is going
forward. I think it's probably a good idea to minify any package that
gets sent over the network. So yes to modules that run in the browser,
and no to modules that run on the server and during development only.
2021-10-01 03:01:28 +08:00
|
|
|
if (!hostContext.isInAParentText) {
|
|
|
|
|
throw new Error('Text strings must be rendered within a <Text> component.');
|
|
|
|
|
}
|
2021-08-17 08:53:23 +08:00
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
const tag = allocateTag();
|
2017-05-25 00:06:30 +08:00
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
UIManager.createView(
|
|
|
|
|
tag, // reactTag
|
|
|
|
|
'RCTRawText', // viewName
|
|
|
|
|
rootContainerInstance, // rootTag
|
|
|
|
|
{text: text}, // props
|
|
|
|
|
);
|
2017-05-25 00:06:30 +08:00
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
precacheFiberNode(internalInstanceHandle, tag);
|
|
|
|
|
|
|
|
|
|
return tag;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function finalizeInitialChildren(
|
|
|
|
|
parentInstance: Instance,
|
|
|
|
|
type: string,
|
|
|
|
|
props: Props,
|
|
|
|
|
rootContainerInstance: Container,
|
|
|
|
|
hostContext: HostContext,
|
|
|
|
|
): boolean {
|
|
|
|
|
// Don't send a no-op message over the bridge.
|
|
|
|
|
if (parentInstance._children.length === 0) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Map from child objects to native tags.
|
|
|
|
|
// Either way we need to pass a copy of the Array to prevent it from being frozen.
|
2020-01-09 21:54:11 +08:00
|
|
|
const nativeTags = parentInstance._children.map(child =>
|
|
|
|
|
typeof child === 'number'
|
|
|
|
|
? child // Leaf node (eg text)
|
|
|
|
|
: child._nativeTag,
|
2018-05-19 18:29:11 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
UIManager.setChildren(
|
|
|
|
|
parentInstance._nativeTag, // containerTag
|
|
|
|
|
nativeTags, // reactTags
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getRootHostContext(
|
|
|
|
|
rootContainerInstance: Container,
|
|
|
|
|
): HostContext {
|
|
|
|
|
return {isInAParentText: false};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getChildHostContext(
|
|
|
|
|
parentHostContext: HostContext,
|
|
|
|
|
type: string,
|
|
|
|
|
rootContainerInstance: Container,
|
|
|
|
|
): HostContext {
|
|
|
|
|
const prevIsInAParentText = parentHostContext.isInAParentText;
|
|
|
|
|
const isInAParentText =
|
|
|
|
|
type === 'AndroidTextInput' || // Android
|
|
|
|
|
type === 'RCTMultilineTextInputView' || // iOS
|
|
|
|
|
type === 'RCTSinglelineTextInputView' || // iOS
|
|
|
|
|
type === 'RCTText' ||
|
|
|
|
|
type === 'RCTVirtualText';
|
|
|
|
|
|
|
|
|
|
if (prevIsInAParentText !== isInAParentText) {
|
|
|
|
|
return {isInAParentText};
|
|
|
|
|
} else {
|
|
|
|
|
return parentHostContext;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getPublicInstance(instance: Instance): * {
|
|
|
|
|
return instance;
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-21 02:32:22 +08:00
|
|
|
export function prepareForCommit(containerInfo: Container): null | Object {
|
2018-05-19 18:29:11 +08:00
|
|
|
// Noop
|
2020-04-21 02:32:22 +08:00
|
|
|
return null;
|
2018-05-19 18:29:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function prepareUpdate(
|
|
|
|
|
instance: Instance,
|
|
|
|
|
type: string,
|
|
|
|
|
oldProps: Props,
|
|
|
|
|
newProps: Props,
|
|
|
|
|
rootContainerInstance: Container,
|
|
|
|
|
hostContext: HostContext,
|
|
|
|
|
): null | Object {
|
Inline fbjs/lib/emptyObject (#13055)
* Inline fbjs/lib/emptyObject
* Explicit naming
* Compare to undefined
* Another approach for detecting whether we can mutate
Each renderer would have its own local LegacyRefsObject function.
While in general we don't want `instanceof`, here it lets us do a simple check: did *we* create the refs object?
Then we can mutate it.
If the check didn't pass, either we're attaching ref for the first time (so we know to use the constructor),
or (unlikely) we're attaching a ref to a component owned by another renderer. In this case, to avoid "losing"
refs, we assign them onto the new object. Even in that case it shouldn't "hop" between renderers anymore.
* Clearer naming
* Add test case for strings refs across renderers
* Use a shared empty object for refs by reading it from React
* Remove string refs from ReactART test
It's not currently possible to resetModules() between several renderers
without also resetting the `React` module. However, that leads to losing
the referential identity of the empty ref object, and thus subsequent
checks in the renderers for whether it is pooled fail (and cause assignments
to a frozen object).
This has always been the case, but we used to work around it by shimming
fbjs/lib/emptyObject in tests and preserving its referential identity.
This won't work anymore because we've inlined it. And preserving referential
identity of React itself wouldn't be great because it could be confusing during
testing (although we might want to revisit this in the future by moving its
stateful parts into a separate package).
For now, I'm removing string ref usage from this test because only this is
the only place in our tests where we hit this problem, and it's only
related to string refs, and not just ref mechanism in general.
* Simplify the condition
2018-06-19 20:41:42 +08:00
|
|
|
return UPDATE_SIGNAL;
|
2018-05-19 18:29:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resetAfterCommit(containerInfo: Container): void {
|
|
|
|
|
// Noop
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const isPrimaryRenderer = true;
|
2019-07-03 05:20:17 +08:00
|
|
|
export const warnsIfNotActing = true;
|
2017-05-25 00:06:30 +08:00
|
|
|
|
2018-07-04 10:22:41 +08:00
|
|
|
export const scheduleTimeout = setTimeout;
|
|
|
|
|
export const cancelTimeout = clearTimeout;
|
|
|
|
|
export const noTimeout = -1;
|
|
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
export function shouldSetTextContent(type: string, props: Props): boolean {
|
|
|
|
|
// TODO (bvaughn) Revisit this decision.
|
|
|
|
|
// Always returning false simplifies the createInstance() implementation,
|
|
|
|
|
// But creates an additional child Fiber for raw text children.
|
|
|
|
|
// No additional native views are created though.
|
|
|
|
|
// It's not clear to me which is better so I'm deferring for now.
|
|
|
|
|
// More context @ github.com/facebook/react/pull/8560#discussion_r92111303
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-10 02:32:20 +08:00
|
|
|
export function getCurrentEventPriority(): * {
|
2021-03-24 04:57:28 +08:00
|
|
|
return DefaultEventPriority;
|
2021-02-10 02:32:20 +08:00
|
|
|
}
|
|
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
// -------------------
|
|
|
|
|
// Mutation
|
|
|
|
|
// -------------------
|
|
|
|
|
|
|
|
|
|
export const supportsMutation = true;
|
|
|
|
|
|
|
|
|
|
export function appendChild(
|
|
|
|
|
parentInstance: Instance,
|
|
|
|
|
child: Instance | TextInstance,
|
|
|
|
|
): void {
|
|
|
|
|
const childTag = typeof child === 'number' ? child : child._nativeTag;
|
|
|
|
|
const children = parentInstance._children;
|
|
|
|
|
const index = children.indexOf(child);
|
|
|
|
|
|
|
|
|
|
if (index >= 0) {
|
|
|
|
|
children.splice(index, 1);
|
|
|
|
|
children.push(child);
|
|
|
|
|
|
|
|
|
|
UIManager.manageChildren(
|
2017-05-25 00:06:30 +08:00
|
|
|
parentInstance._nativeTag, // containerTag
|
2018-05-19 18:29:11 +08:00
|
|
|
[index], // moveFromIndices
|
|
|
|
|
[children.length - 1], // moveToIndices
|
|
|
|
|
[], // addChildReactTags
|
|
|
|
|
[], // addAtIndices
|
|
|
|
|
[], // removeAtIndices
|
2017-05-25 00:06:30 +08:00
|
|
|
);
|
2018-05-19 18:29:11 +08:00
|
|
|
} else {
|
|
|
|
|
children.push(child);
|
2017-05-25 00:06:30 +08:00
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
UIManager.manageChildren(
|
|
|
|
|
parentInstance._nativeTag, // containerTag
|
|
|
|
|
[], // moveFromIndices
|
|
|
|
|
[], // moveToIndices
|
|
|
|
|
[childTag], // addChildReactTags
|
|
|
|
|
[children.length - 1], // addAtIndices
|
|
|
|
|
[], // removeAtIndices
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-05-25 00:06:30 +08:00
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
export function appendChildToContainer(
|
|
|
|
|
parentInstance: Container,
|
|
|
|
|
child: Instance | TextInstance,
|
|
|
|
|
): void {
|
|
|
|
|
const childTag = typeof child === 'number' ? child : child._nativeTag;
|
|
|
|
|
UIManager.setChildren(
|
|
|
|
|
parentInstance, // containerTag
|
|
|
|
|
[childTag], // reactTags
|
|
|
|
|
);
|
|
|
|
|
}
|
2017-05-25 00:06:30 +08:00
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
export function commitTextUpdate(
|
|
|
|
|
textInstance: TextInstance,
|
|
|
|
|
oldText: string,
|
|
|
|
|
newText: string,
|
|
|
|
|
): void {
|
|
|
|
|
UIManager.updateView(
|
|
|
|
|
textInstance, // reactTag
|
|
|
|
|
'RCTRawText', // viewName
|
|
|
|
|
{text: newText}, // props
|
|
|
|
|
);
|
|
|
|
|
}
|
2017-10-26 05:13:34 +08:00
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
export function commitMount(
|
|
|
|
|
instance: Instance,
|
|
|
|
|
type: string,
|
|
|
|
|
newProps: Props,
|
|
|
|
|
internalInstanceHandle: Object,
|
|
|
|
|
): void {
|
|
|
|
|
// Noop
|
|
|
|
|
}
|
2018-05-11 09:34:01 +08:00
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
export function commitUpdate(
|
|
|
|
|
instance: Instance,
|
|
|
|
|
updatePayloadTODO: Object,
|
|
|
|
|
type: string,
|
|
|
|
|
oldProps: Props,
|
|
|
|
|
newProps: Props,
|
|
|
|
|
internalInstanceHandle: Object,
|
|
|
|
|
): void {
|
|
|
|
|
const viewConfig = instance.viewConfig;
|
|
|
|
|
|
|
|
|
|
updateFiberProps(instance._nativeTag, newProps);
|
|
|
|
|
|
2018-11-20 07:32:54 +08:00
|
|
|
const updatePayload = diff(oldProps, newProps, viewConfig.validAttributes);
|
2018-05-19 18:29:11 +08:00
|
|
|
|
|
|
|
|
// Avoid the overhead of bridge calls if there's no update.
|
|
|
|
|
// This is an expensive no-op for Android, and causes an unnecessary
|
|
|
|
|
// view invalidation for certain components (eg RCTTextInput) on iOS.
|
|
|
|
|
if (updatePayload != null) {
|
|
|
|
|
UIManager.updateView(
|
|
|
|
|
instance._nativeTag, // reactTag
|
|
|
|
|
viewConfig.uiViewClassName, // viewName
|
|
|
|
|
updatePayload, // props
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-05-25 00:06:30 +08:00
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
export function insertBefore(
|
|
|
|
|
parentInstance: Instance,
|
|
|
|
|
child: Instance | TextInstance,
|
|
|
|
|
beforeChild: Instance | TextInstance,
|
|
|
|
|
): void {
|
|
|
|
|
const children = (parentInstance: any)._children;
|
|
|
|
|
const index = children.indexOf(child);
|
|
|
|
|
|
|
|
|
|
// Move existing child or add new child?
|
|
|
|
|
if (index >= 0) {
|
|
|
|
|
children.splice(index, 1);
|
|
|
|
|
const beforeChildIndex = children.indexOf(beforeChild);
|
|
|
|
|
children.splice(beforeChildIndex, 0, child);
|
|
|
|
|
|
|
|
|
|
UIManager.manageChildren(
|
|
|
|
|
(parentInstance: any)._nativeTag, // containerID
|
|
|
|
|
[index], // moveFromIndices
|
|
|
|
|
[beforeChildIndex], // moveToIndices
|
|
|
|
|
[], // addChildReactTags
|
|
|
|
|
[], // addAtIndices
|
|
|
|
|
[], // removeAtIndices
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
const beforeChildIndex = children.indexOf(beforeChild);
|
|
|
|
|
children.splice(beforeChildIndex, 0, child);
|
|
|
|
|
|
|
|
|
|
const childTag = typeof child === 'number' ? child : child._nativeTag;
|
|
|
|
|
|
|
|
|
|
UIManager.manageChildren(
|
|
|
|
|
(parentInstance: any)._nativeTag, // containerID
|
|
|
|
|
[], // moveFromIndices
|
|
|
|
|
[], // moveToIndices
|
|
|
|
|
[childTag], // addChildReactTags
|
|
|
|
|
[beforeChildIndex], // addAtIndices
|
|
|
|
|
[], // removeAtIndices
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-05-25 00:06:30 +08:00
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
export function insertInContainerBefore(
|
|
|
|
|
parentInstance: Container,
|
|
|
|
|
child: Instance | TextInstance,
|
|
|
|
|
beforeChild: Instance | TextInstance,
|
|
|
|
|
): void {
|
|
|
|
|
// TODO (bvaughn): Remove this check when...
|
|
|
|
|
// We create a wrapper object for the container in ReactNative render()
|
|
|
|
|
// Or we refactor to remove wrapper objects entirely.
|
|
|
|
|
// For more info on pros/cons see PR #8560 description.
|
[RFC] Codemod invariant -> throw new Error (#22435)
* Hoist error codes import to module scope
When this code was written, the error codes map (`codes.json`) was
created on-the-fly, so we had to lazily require from inside the visitor.
Because `codes.json` is now checked into source, we can import it a
single time in module scope.
* Minify error constructors in production
We use a script to minify our error messages in production. Each message
is assigned an error code, defined in `scripts/error-codes/codes.json`.
Then our build script replaces the messages with a link to our
error decoder page, e.g. https://reactjs.org/docs/error-decoder.html/?invariant=92
This enables us to write helpful error messages without increasing the
bundle size.
Right now, the script only works for `invariant` calls. It does not work
if you throw an Error object. This is an old Facebookism that we don't
really need, other than the fact that our error minification script
relies on it.
So, I've updated the script to minify error constructors, too:
Input:
Error(`A ${adj} message that contains ${noun}`);
Output:
Error(formatProdErrorMessage(ERR_CODE, adj, noun));
It only works for constructors that are literally named Error, though we
could add support for other names, too.
As a next step, I will add a lint rule to enforce that errors written
this way must have a corresponding error code.
* Minify "no fallback UI specified" error in prod
This error message wasn't being minified because it doesn't use
invariant. The reason it didn't use invariant is because this particular
error is created without begin thrown — it doesn't need to be thrown
because it's located inside the error handling part of the runtime.
Now that the error minification script supports Error constructors, we
can minify it by assigning it a production error code in
`scripts/error-codes/codes.json`.
To support the use of Error constructors more generally, I will add a
lint rule that enforces each message has a corresponding error code.
* Lint rule to detect unminified errors
Adds a lint rule that detects when an Error constructor is used without
a corresponding production error code.
We already have this for `invariant`, but not for regular errors, i.e.
`throw new Error(msg)`. There's also nothing that enforces the use of
`invariant` besides convention.
There are some packages where we don't care to minify errors. These are
packages that run in environments where bundle size is not a concern,
like react-pg. I added an override in the ESLint config to ignore these.
* Temporarily add invariant codemod script
I'm adding this codemod to the repo temporarily, but I'll revert it
in the same PR. That way we don't have to check it in but it's still
accessible (via the PR) if we need it later.
* [Automated] Codemod invariant -> Error
This commit contains only automated changes:
npx jscodeshift -t scripts/codemod-invariant.js packages --ignore-pattern="node_modules/**/*"
yarn linc --fix
yarn prettier
I will do any manual touch ups in separate commits so they're easier
to review.
* Remove temporary codemod script
This reverts the codemod script and ESLint config I added temporarily
in order to perform the invariant codemod.
* Manual touch ups
A few manual changes I made after the codemod ran.
* Enable error code transform per package
Currently we're not consistent about which packages should have their
errors minified in production and which ones should.
This adds a field to the bundle configuration to control whether to
apply the transform. We should decide what the criteria is going
forward. I think it's probably a good idea to minify any package that
gets sent over the network. So yes to modules that run in the browser,
and no to modules that run on the server and during development only.
2021-10-01 03:01:28 +08:00
|
|
|
if (typeof parentInstance === 'number') {
|
|
|
|
|
throw new Error('Container does not support insertBefore operation');
|
|
|
|
|
}
|
2018-05-19 18:29:11 +08:00
|
|
|
}
|
2017-05-25 00:06:30 +08:00
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
export function removeChild(
|
|
|
|
|
parentInstance: Instance,
|
|
|
|
|
child: Instance | TextInstance,
|
|
|
|
|
): void {
|
|
|
|
|
recursivelyUncacheFiberNode(child);
|
|
|
|
|
const children = parentInstance._children;
|
|
|
|
|
const index = children.indexOf(child);
|
|
|
|
|
|
|
|
|
|
children.splice(index, 1);
|
|
|
|
|
|
|
|
|
|
UIManager.manageChildren(
|
|
|
|
|
parentInstance._nativeTag, // containerID
|
|
|
|
|
[], // moveFromIndices
|
|
|
|
|
[], // moveToIndices
|
|
|
|
|
[], // addChildReactTags
|
|
|
|
|
[], // addAtIndices
|
|
|
|
|
[index], // removeAtIndices
|
|
|
|
|
);
|
|
|
|
|
}
|
2017-10-26 05:13:34 +08:00
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
export function removeChildFromContainer(
|
|
|
|
|
parentInstance: Container,
|
|
|
|
|
child: Instance | TextInstance,
|
|
|
|
|
): void {
|
|
|
|
|
recursivelyUncacheFiberNode(child);
|
|
|
|
|
UIManager.manageChildren(
|
|
|
|
|
parentInstance, // containerID
|
|
|
|
|
[], // moveFromIndices
|
|
|
|
|
[], // moveToIndices
|
|
|
|
|
[], // addChildReactTags
|
|
|
|
|
[], // addAtIndices
|
|
|
|
|
[0], // removeAtIndices
|
|
|
|
|
);
|
|
|
|
|
}
|
2017-05-25 00:06:30 +08:00
|
|
|
|
2018-05-19 18:29:11 +08:00
|
|
|
export function resetTextContent(instance: Instance): void {
|
|
|
|
|
// Noop
|
|
|
|
|
}
|
Hide timed-out children instead of deleting them so their state is preserved (#13823)
* Store the start time on `updateQueue` instead of `stateNode`
Originally I did this to free the `stateNode` field to store a second
set of children. I don't we'll need this anymore, since we use fragment
fibers instead. But I still think using `updateQueue` makes more sense
so I'll leave this in.
* Use fragment fibers to keep the primary and fallback children separate
If the children timeout, we switch to showing the fallback children in
place of the "primary" children. However, we don't want to delete the
primary children because then their state will be lost (both the React
state and the host state, e.g. uncontrolled form inputs). Instead we
keep them mounted and hide them. Both the fallback children AND the
primary children are rendered at the same time. Once the primary
children are un-suspended, we can delete the fallback children — don't
need to preserve their state.
The two sets of children are siblings in the host environment, but
semantically, for purposes of reconciliation, they are two separate
sets. So we store them using two fragment fibers.
However, we want to avoid allocating extra fibers for every placeholder.
They're only necessary when the children time out, because that's the
only time when both sets are mounted.
So, the extra fragment fibers are only used if the children time out.
Otherwise, we render the primary children directly. This requires some
custom reconciliation logic to preserve the state of the primary
children. It's essentially a very basic form of re-parenting.
* Use `memoizedState` to store various pieces of SuspenseComponent's state
SuspenseComponent has three pieces of state:
- alreadyCaptured: Whether a component in the child subtree already
suspended. If true, subsequent suspends should bubble up to the
next boundary.
- didTimeout: Whether the boundary renders the primary or fallback
children. This is separate from `alreadyCaptured` because outside of
strict mode, when a boundary times out, the first commit renders the
primary children in an incomplete state, then performs a second commit
to switch the fallback. In that first commit, `alreadyCaptured` is
false and `didTimeout` is true.
- timedOutAt: The time at which the boundary timed out. This is separate
from `didTimeout` because it's not set unless the boundary
actually commits.
These were previously spread across several fields.
This happens to make the non-strict case a bit less hacky; the logic for
that special case is now mostly localized to the UnwindWork module.
* Hide timed-out Suspense children
When a subtree takes too long to load, we swap its contents out for
a fallback to unblock the rest of the tree. Because we don't want
to lose the state of the timed out view, we shouldn't actually delete
the nodes from the tree. Instead, we'll keep them mounted and hide
them visually. When the subtree is unblocked, we un-hide it, having
preserved the existing state.
Adds additional host config methods. For mutation mode:
- hideInstance
- hideTextInstance
- unhideInstance
- unhideTextInstance
For persistent mode:
- cloneHiddenInstance
- cloneUnhiddenInstance
- createHiddenTextInstance
I've only implemented the new methods in the noop and test renderers.
I'll implement them in the other renderers in subsequent commits.
* Include `hidden` prop in noop renderer's output
This will be used in subsequent commits to test that timed-out children
are properly hidden.
Also adds getChildrenAsJSX() method as an alternative to using
getChildren(). (Ideally all our tests would use test renderer #oneday.)
* Implement hide/unhide host config methods for DOM renderer
For DOM nodes, we hide using `el.style.display = 'none'`.
Text nodes don't have style, so we hide using `text.textContent = ''`.
* Implement hide/unhide host config methods for Art renderer
* Create DOM fixture that tests state preservation of timed out content
* Account for class components that suspend outside concurrent mode
Need to distinguish mount from update. An unfortunate edge case :(
* Fork appendAllChildren between persistent and mutation mode
* Remove redundant check for existence of el.style
* Schedule placement effect on indeterminate components
In non-concurrent mode, indeterminate fibers may commit in an
inconsistent state. But when they update, we should throw out the
old fiber and start fresh. Which means the new fiber needs a
placement effect.
* Pass null instead of current everywhere in mountIndeterminateComponent
2018-10-19 06:37:16 +08:00
|
|
|
|
|
|
|
|
export function hideInstance(instance: Instance): void {
|
2018-11-06 07:33:25 +08:00
|
|
|
const viewConfig = instance.viewConfig;
|
2018-11-20 07:32:54 +08:00
|
|
|
const updatePayload = create(
|
2018-11-06 07:33:25 +08:00
|
|
|
{style: {display: 'none'}},
|
|
|
|
|
viewConfig.validAttributes,
|
|
|
|
|
);
|
|
|
|
|
UIManager.updateView(
|
|
|
|
|
instance._nativeTag,
|
|
|
|
|
viewConfig.uiViewClassName,
|
|
|
|
|
updatePayload,
|
|
|
|
|
);
|
Hide timed-out children instead of deleting them so their state is preserved (#13823)
* Store the start time on `updateQueue` instead of `stateNode`
Originally I did this to free the `stateNode` field to store a second
set of children. I don't we'll need this anymore, since we use fragment
fibers instead. But I still think using `updateQueue` makes more sense
so I'll leave this in.
* Use fragment fibers to keep the primary and fallback children separate
If the children timeout, we switch to showing the fallback children in
place of the "primary" children. However, we don't want to delete the
primary children because then their state will be lost (both the React
state and the host state, e.g. uncontrolled form inputs). Instead we
keep them mounted and hide them. Both the fallback children AND the
primary children are rendered at the same time. Once the primary
children are un-suspended, we can delete the fallback children — don't
need to preserve their state.
The two sets of children are siblings in the host environment, but
semantically, for purposes of reconciliation, they are two separate
sets. So we store them using two fragment fibers.
However, we want to avoid allocating extra fibers for every placeholder.
They're only necessary when the children time out, because that's the
only time when both sets are mounted.
So, the extra fragment fibers are only used if the children time out.
Otherwise, we render the primary children directly. This requires some
custom reconciliation logic to preserve the state of the primary
children. It's essentially a very basic form of re-parenting.
* Use `memoizedState` to store various pieces of SuspenseComponent's state
SuspenseComponent has three pieces of state:
- alreadyCaptured: Whether a component in the child subtree already
suspended. If true, subsequent suspends should bubble up to the
next boundary.
- didTimeout: Whether the boundary renders the primary or fallback
children. This is separate from `alreadyCaptured` because outside of
strict mode, when a boundary times out, the first commit renders the
primary children in an incomplete state, then performs a second commit
to switch the fallback. In that first commit, `alreadyCaptured` is
false and `didTimeout` is true.
- timedOutAt: The time at which the boundary timed out. This is separate
from `didTimeout` because it's not set unless the boundary
actually commits.
These were previously spread across several fields.
This happens to make the non-strict case a bit less hacky; the logic for
that special case is now mostly localized to the UnwindWork module.
* Hide timed-out Suspense children
When a subtree takes too long to load, we swap its contents out for
a fallback to unblock the rest of the tree. Because we don't want
to lose the state of the timed out view, we shouldn't actually delete
the nodes from the tree. Instead, we'll keep them mounted and hide
them visually. When the subtree is unblocked, we un-hide it, having
preserved the existing state.
Adds additional host config methods. For mutation mode:
- hideInstance
- hideTextInstance
- unhideInstance
- unhideTextInstance
For persistent mode:
- cloneHiddenInstance
- cloneUnhiddenInstance
- createHiddenTextInstance
I've only implemented the new methods in the noop and test renderers.
I'll implement them in the other renderers in subsequent commits.
* Include `hidden` prop in noop renderer's output
This will be used in subsequent commits to test that timed-out children
are properly hidden.
Also adds getChildrenAsJSX() method as an alternative to using
getChildren(). (Ideally all our tests would use test renderer #oneday.)
* Implement hide/unhide host config methods for DOM renderer
For DOM nodes, we hide using `el.style.display = 'none'`.
Text nodes don't have style, so we hide using `text.textContent = ''`.
* Implement hide/unhide host config methods for Art renderer
* Create DOM fixture that tests state preservation of timed out content
* Account for class components that suspend outside concurrent mode
Need to distinguish mount from update. An unfortunate edge case :(
* Fork appendAllChildren between persistent and mutation mode
* Remove redundant check for existence of el.style
* Schedule placement effect on indeterminate components
In non-concurrent mode, indeterminate fibers may commit in an
inconsistent state. But when they update, we should throw out the
old fiber and start fresh. Which means the new fiber needs a
placement effect.
* Pass null instead of current everywhere in mountIndeterminateComponent
2018-10-19 06:37:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function hideTextInstance(textInstance: TextInstance): void {
|
|
|
|
|
throw new Error('Not yet implemented.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function unhideInstance(instance: Instance, props: Props): void {
|
2018-11-06 07:33:25 +08:00
|
|
|
const viewConfig = instance.viewConfig;
|
2018-11-20 07:32:54 +08:00
|
|
|
const updatePayload = diff(
|
2018-11-06 07:33:25 +08:00
|
|
|
{...props, style: [props.style, {display: 'none'}]},
|
|
|
|
|
props,
|
|
|
|
|
viewConfig.validAttributes,
|
|
|
|
|
);
|
|
|
|
|
UIManager.updateView(
|
|
|
|
|
instance._nativeTag,
|
|
|
|
|
viewConfig.uiViewClassName,
|
|
|
|
|
updatePayload,
|
|
|
|
|
);
|
Hide timed-out children instead of deleting them so their state is preserved (#13823)
* Store the start time on `updateQueue` instead of `stateNode`
Originally I did this to free the `stateNode` field to store a second
set of children. I don't we'll need this anymore, since we use fragment
fibers instead. But I still think using `updateQueue` makes more sense
so I'll leave this in.
* Use fragment fibers to keep the primary and fallback children separate
If the children timeout, we switch to showing the fallback children in
place of the "primary" children. However, we don't want to delete the
primary children because then their state will be lost (both the React
state and the host state, e.g. uncontrolled form inputs). Instead we
keep them mounted and hide them. Both the fallback children AND the
primary children are rendered at the same time. Once the primary
children are un-suspended, we can delete the fallback children — don't
need to preserve their state.
The two sets of children are siblings in the host environment, but
semantically, for purposes of reconciliation, they are two separate
sets. So we store them using two fragment fibers.
However, we want to avoid allocating extra fibers for every placeholder.
They're only necessary when the children time out, because that's the
only time when both sets are mounted.
So, the extra fragment fibers are only used if the children time out.
Otherwise, we render the primary children directly. This requires some
custom reconciliation logic to preserve the state of the primary
children. It's essentially a very basic form of re-parenting.
* Use `memoizedState` to store various pieces of SuspenseComponent's state
SuspenseComponent has three pieces of state:
- alreadyCaptured: Whether a component in the child subtree already
suspended. If true, subsequent suspends should bubble up to the
next boundary.
- didTimeout: Whether the boundary renders the primary or fallback
children. This is separate from `alreadyCaptured` because outside of
strict mode, when a boundary times out, the first commit renders the
primary children in an incomplete state, then performs a second commit
to switch the fallback. In that first commit, `alreadyCaptured` is
false and `didTimeout` is true.
- timedOutAt: The time at which the boundary timed out. This is separate
from `didTimeout` because it's not set unless the boundary
actually commits.
These were previously spread across several fields.
This happens to make the non-strict case a bit less hacky; the logic for
that special case is now mostly localized to the UnwindWork module.
* Hide timed-out Suspense children
When a subtree takes too long to load, we swap its contents out for
a fallback to unblock the rest of the tree. Because we don't want
to lose the state of the timed out view, we shouldn't actually delete
the nodes from the tree. Instead, we'll keep them mounted and hide
them visually. When the subtree is unblocked, we un-hide it, having
preserved the existing state.
Adds additional host config methods. For mutation mode:
- hideInstance
- hideTextInstance
- unhideInstance
- unhideTextInstance
For persistent mode:
- cloneHiddenInstance
- cloneUnhiddenInstance
- createHiddenTextInstance
I've only implemented the new methods in the noop and test renderers.
I'll implement them in the other renderers in subsequent commits.
* Include `hidden` prop in noop renderer's output
This will be used in subsequent commits to test that timed-out children
are properly hidden.
Also adds getChildrenAsJSX() method as an alternative to using
getChildren(). (Ideally all our tests would use test renderer #oneday.)
* Implement hide/unhide host config methods for DOM renderer
For DOM nodes, we hide using `el.style.display = 'none'`.
Text nodes don't have style, so we hide using `text.textContent = ''`.
* Implement hide/unhide host config methods for Art renderer
* Create DOM fixture that tests state preservation of timed out content
* Account for class components that suspend outside concurrent mode
Need to distinguish mount from update. An unfortunate edge case :(
* Fork appendAllChildren between persistent and mutation mode
* Remove redundant check for existence of el.style
* Schedule placement effect on indeterminate components
In non-concurrent mode, indeterminate fibers may commit in an
inconsistent state. But when they update, we should throw out the
old fiber and start fresh. Which means the new fiber needs a
placement effect.
* Pass null instead of current everywhere in mountIndeterminateComponent
2018-10-19 06:37:16 +08:00
|
|
|
}
|
|
|
|
|
|
2020-04-29 04:07:42 +08:00
|
|
|
export function clearContainer(container: Container): void {
|
|
|
|
|
// TODO Implement this for React Native
|
|
|
|
|
// UIManager does not expose a "remove all" type method.
|
|
|
|
|
}
|
|
|
|
|
|
Hide timed-out children instead of deleting them so their state is preserved (#13823)
* Store the start time on `updateQueue` instead of `stateNode`
Originally I did this to free the `stateNode` field to store a second
set of children. I don't we'll need this anymore, since we use fragment
fibers instead. But I still think using `updateQueue` makes more sense
so I'll leave this in.
* Use fragment fibers to keep the primary and fallback children separate
If the children timeout, we switch to showing the fallback children in
place of the "primary" children. However, we don't want to delete the
primary children because then their state will be lost (both the React
state and the host state, e.g. uncontrolled form inputs). Instead we
keep them mounted and hide them. Both the fallback children AND the
primary children are rendered at the same time. Once the primary
children are un-suspended, we can delete the fallback children — don't
need to preserve their state.
The two sets of children are siblings in the host environment, but
semantically, for purposes of reconciliation, they are two separate
sets. So we store them using two fragment fibers.
However, we want to avoid allocating extra fibers for every placeholder.
They're only necessary when the children time out, because that's the
only time when both sets are mounted.
So, the extra fragment fibers are only used if the children time out.
Otherwise, we render the primary children directly. This requires some
custom reconciliation logic to preserve the state of the primary
children. It's essentially a very basic form of re-parenting.
* Use `memoizedState` to store various pieces of SuspenseComponent's state
SuspenseComponent has three pieces of state:
- alreadyCaptured: Whether a component in the child subtree already
suspended. If true, subsequent suspends should bubble up to the
next boundary.
- didTimeout: Whether the boundary renders the primary or fallback
children. This is separate from `alreadyCaptured` because outside of
strict mode, when a boundary times out, the first commit renders the
primary children in an incomplete state, then performs a second commit
to switch the fallback. In that first commit, `alreadyCaptured` is
false and `didTimeout` is true.
- timedOutAt: The time at which the boundary timed out. This is separate
from `didTimeout` because it's not set unless the boundary
actually commits.
These were previously spread across several fields.
This happens to make the non-strict case a bit less hacky; the logic for
that special case is now mostly localized to the UnwindWork module.
* Hide timed-out Suspense children
When a subtree takes too long to load, we swap its contents out for
a fallback to unblock the rest of the tree. Because we don't want
to lose the state of the timed out view, we shouldn't actually delete
the nodes from the tree. Instead, we'll keep them mounted and hide
them visually. When the subtree is unblocked, we un-hide it, having
preserved the existing state.
Adds additional host config methods. For mutation mode:
- hideInstance
- hideTextInstance
- unhideInstance
- unhideTextInstance
For persistent mode:
- cloneHiddenInstance
- cloneUnhiddenInstance
- createHiddenTextInstance
I've only implemented the new methods in the noop and test renderers.
I'll implement them in the other renderers in subsequent commits.
* Include `hidden` prop in noop renderer's output
This will be used in subsequent commits to test that timed-out children
are properly hidden.
Also adds getChildrenAsJSX() method as an alternative to using
getChildren(). (Ideally all our tests would use test renderer #oneday.)
* Implement hide/unhide host config methods for DOM renderer
For DOM nodes, we hide using `el.style.display = 'none'`.
Text nodes don't have style, so we hide using `text.textContent = ''`.
* Implement hide/unhide host config methods for Art renderer
* Create DOM fixture that tests state preservation of timed out content
* Account for class components that suspend outside concurrent mode
Need to distinguish mount from update. An unfortunate edge case :(
* Fork appendAllChildren between persistent and mutation mode
* Remove redundant check for existence of el.style
* Schedule placement effect on indeterminate components
In non-concurrent mode, indeterminate fibers may commit in an
inconsistent state. But when they update, we should throw out the
old fiber and start fresh. Which means the new fiber needs a
placement effect.
* Pass null instead of current everywhere in mountIndeterminateComponent
2018-10-19 06:37:16 +08:00
|
|
|
export function unhideTextInstance(
|
|
|
|
|
textInstance: TextInstance,
|
|
|
|
|
text: string,
|
|
|
|
|
): void {
|
|
|
|
|
throw new Error('Not yet implemented.');
|
|
|
|
|
}
|
2019-03-15 01:02:42 +08:00
|
|
|
|
2020-01-23 02:02:30 +08:00
|
|
|
export function getInstanceFromNode(node: any) {
|
2019-10-09 01:32:53 +08:00
|
|
|
throw new Error('Not yet implemented.');
|
|
|
|
|
}
|
2019-11-14 04:46:00 +08:00
|
|
|
|
2020-10-15 06:38:57 +08:00
|
|
|
export function beforeActiveInstanceBlur(internalInstanceHandle: Object) {
|
2020-04-21 02:32:22 +08:00
|
|
|
// noop
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function afterActiveInstanceBlur() {
|
|
|
|
|
// noop
|
|
|
|
|
}
|
2020-04-24 03:57:52 +08:00
|
|
|
|
|
|
|
|
export function preparePortalMount(portalInstance: Instance): void {
|
|
|
|
|
// noop
|
|
|
|
|
}
|
2021-03-23 12:54:53 +08:00
|
|
|
|
|
|
|
|
export function detachDeletedInstance(node: Instance): void {
|
|
|
|
|
// noop
|
|
|
|
|
}
|