Add <ViewTransition> Component (#31975)

This will provide the opt-in for using [View
Transitions](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API)
in React.

View Transitions only trigger for async updates like `startTransition`,
`useDeferredValue`, Actions or `<Suspense>` revealing from fallback to
content. Synchronous updates provide an opt-out but also guarantee that
they commit immediately which View Transitions can't.

There's no need to opt-in to View Transitions at the "cause" side like
event handlers or actions. They don't know what UI will change and
whether that has an animated transition described.

Conceptually the `<ViewTransition>` component is like a DOM fragment
that transitions its children in its own isolate/snapshot. The API works
by wrapping a DOM node or inner component:

```js
import {ViewTransition} from 'react';

<ViewTransition><Component /></ViewTransition>
```

The default is `name="auto"` which will automatically assign a
`view-transition-name` to the inner DOM node. That way you can add a
View Transition to a Component without controlling its DOM nodes styling
otherwise.

A difference between this and the browser's built-in
`view-transition-name: auto` is that switching the DOM nodes within the
`<ViewTransition>` component preserves the same name so this example
cross-fades between the DOM nodes instead of causing an exit and enter:

```js
<ViewTransition>{condition ? <ComponentA /> : <ComponentB />}</ViewTransition>
```

This becomes especially useful with `<Suspense>` as this example
cross-fades between Skeleton and Content:

```js
<ViewTransition>
  <Suspense fallback={<Skeleton />}>
    <Content />
  </Suspense>
</ViewTransition>
```

Where as this example triggers an exit of the Skeleton and an enter of
the Content:

```js
<Suspense fallback={<ViewTransition><Skeleton /></ViewTransition>}>
  <ViewTransition><Content /></ViewTransition>
</Suspense>
```

Managing instances and keys becomes extra important.

You can also specify an explicit `name` property for example for
animating the same conceptual item from one page onto another. However,
best practices is to property namespace these since they can easily
collide. It's also useful to add an `id` to it if available.

```js
<ViewTransition name="my-shared-view">
```

The model in general is the same as plain `view-transition-name` except
React manages a set of heuristics for when to apply it. A problem with
the naive View Transitions model is that it overly opts in every
boundary that *might* transition into transitioning. This is leads to
unfortunate effects like things floating around when unrelated updates
happen. This leads the whole document to animate which means that
nothing is clickable in the meantime. It makes it not useful for smaller
and more local transitions. Best practice is to add
`view-transition-name` only right before you're about to need to animate
the thing. This is tricky to manage globally on complex apps and is not
compositional. Instead we let React manage when a `<ViewTransition>`
"activates" and add/remove the `view-transition-name`. This is also when
React calls `startViewTransition` behind the scenes while it mutates the
DOM.

I've come up with a number of heuristics that I think will make a lot
easier to coordinate this. The principle is that only if something that
updates that particular boundary do we activate it. I hope that one day
maybe browsers will have something like these built-in and we can remove
our implementation.

A `<ViewTransition>` only activates if:

- If a mounted Component renders a `<ViewTransition>` within it outside
the first DOM node, and it is within the viewport, then that
ViewTransition activates as an "enter" animation. This avoids inner
"enter" animations trigger when the parent mounts.
- If an unmounted Component had a `<ViewTransition>` within it outside
the first DOM node, and it was within the viewport, then that
ViewTransition activates as an "exit" animation. This avoids inner
"exit" animations triggering when the parent unmounts.
- If an explicitly named `<ViewTransition name="...">` is deep within an
unmounted tree and one with the same name appears in a mounted tree at
the same time, then both are activated as a pair, but only if they're
both in the viewport. This avoids these triggering "enter" or "exit"
animations when going between parents that don't have a pair.
- If an already mounted `<ViewTransition>` is visible and a DOM
mutation, that might affect how it's painted, happens within its
children but outside any nested `<ViewTransition>`. This allows it to
"cross-fade" between its updates.
- If an already mounted `<ViewTransition>` resizes or moves as the
result of direct DOM nodes siblings changing or moving around. This
allows insertion, deletion and reorders into a list to animate all
children. It is only within one DOM node though, to avoid unrelated
changes in the parent to trigger this. If an item is outside the
viewport before and after, then it's skipped to avoid things flying
across the screen.
- If a `<ViewTransition>` boundary changes size, due to a DOM mutation
within it, then the parent activates (or the root document if there are
no more parents). This ensures that the container can cross-fade to
avoid abrupt relayout. This can be avoided by using absolutely
positioned children. When this can avoid bubbling to the root document,
whatever is not animating is still responsive to clicks during the
transition.

Conceptually each DOM node has its own default that activates the parent
`<ViewTransition>` or no transition if the parent is the root. That
means that if you add a DOM node like `<div><ViewTransition><Component
/></ViewTransition></div>` this won't trigger an "enter" animation since
it was the div that was added, not the ViewTransition. Instead, it might
cause a cross-fade of the parent ViewTransition or no transition if it
had no parent. This ensures that only explicit boundaries perform coarse
animations instead of every single node which is really the benefit of
the View Transitions model. This ends up working out well for simple
cases like switching between two pages immediately while transitioning
one floating item that appears on both pages. Because only the floating
item transitions by default.

Note that it's possible to add manual `view-transition-name` with CSS or
`style={{ viewTransitionName: 'auto' }}` that always transitions as long
as something else has a `<ViewTransition>` that activates. For example a
`<ViewTransition>` can wrap a whole page for a cross-fade but inside of
it an explicit name can be added to something to ensure it animates as a
move when something relates else changes its layout. Instead of just
cross-fading it along with the Page which would be the default.

There's more PRs coming with some optimizations, fixes and expanded
APIs. This first PR explores the above core heuristic.

---------

Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>
This commit is contained in:
Sebastian Markbåge
2025-01-08 12:11:18 -05:00
committed by GitHub
co-authored by Sebastian Sebbie Silbermann
parent e30c6693e4
commit a4d122f2d1
57 changed files with 9474 additions and 99 deletions
+30
View File
@@ -0,0 +1,30 @@
# View Transition
A test case for View Transitions.
## Setup
To reference a local build of React, first run `npm run build` at the root
of the React project. Then:
```
cd fixtures/view-transition
yarn
yarn start
```
The `start` command runs a webpack dev server and a server-side rendering server in development mode with hot reloading.
**Note: whenever you make changes to React and rebuild it, you need to re-run `yarn` in this folder:**
```
yarn
```
If you want to try the production mode instead run:
```
yarn start:prod
```
This will pre-build all static resources and then start a server-side rendering HTTP server that hosts the React app and service the static resources (without hot reloading).
+28
View File
@@ -0,0 +1,28 @@
{
"name": "react-fixtures-view-transition",
"version": "0.1.0",
"private": true,
"devDependencies": {
"concurrently": "3.1.0",
"http-proxy-middleware": "0.17.3",
"react-scripts": "0.9.5"
},
"dependencies": {
"express": "^4.14.0",
"ignore-styles": "^5.0.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"scripts": {
"predev": "cp -r ../../build/oss-experimental/* ./node_modules/",
"prestart": "cp -r ../../build/oss-experimental/* ./node_modules/",
"prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/",
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
"dev:client": "PORT=3001 react-scripts start",
"dev:server": "NODE_ENV=development node server",
"start": "react-scripts build && NODE_ENV=production node server",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -0,0 +1,13 @@
<!doctype html>
<html>
<body>
<script>
/*
This is just a placeholder to make react-scripts happy.
We're not using it. If we end up here, redirect to the
primary server.
*/
location.href = '//localhost:3000/';
</script>
</body>
</html>
+70
View File
@@ -0,0 +1,70 @@
require('ignore-styles');
const babelRegister = require('babel-register');
const proxy = require('http-proxy-middleware');
babelRegister({
ignore: /\/(build|node_modules)\//,
presets: ['react-app'],
});
const express = require('express');
const path = require('path');
const app = express();
// Application
if (process.env.NODE_ENV === 'development') {
app.get('/', function (req, res) {
// In development mode we clear the module cache between each request to
// get automatic hot reloading.
for (var key in require.cache) {
delete require.cache[key];
}
const render = require('./render').default;
render(req.url, res);
});
} else {
const render = require('./render').default;
app.get('/', function (req, res) {
render(req.url, res);
});
}
// Static resources
app.use(express.static(path.resolve(__dirname, '..', 'build')));
// Proxy everything else to create-react-app's webpack development server
if (process.env.NODE_ENV === 'development') {
app.use(
'/',
proxy({
ws: true,
target: 'http://localhost:3001',
})
);
}
app.listen(3000, () => {
console.log('Listening on port 3000...');
});
app.on('error', function (error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port;
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
});
+44
View File
@@ -0,0 +1,44 @@
import React from 'react';
import {renderToPipeableStream} from 'react-dom/server';
import App from '../src/components/App';
let assets;
if (process.env.NODE_ENV === 'development') {
// Use the bundle from create-react-app's server in development mode.
assets = {
'main.js': '/static/js/bundle.js',
// 'main.css': '',
};
} else {
assets = require('../build/asset-manifest.json');
}
export default function render(url, res) {
res.socket.on('error', error => {
// Log fatal errors
console.error('Fatal', error);
});
let didError = false;
const {pipe, abort} = renderToPipeableStream(<App assets={assets} />, {
bootstrapScripts: [assets['main.js']],
onShellReady() {
// If something errored before we started streaming, we set the error code appropriately.
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-type', 'text/html');
pipe(res);
},
onShellError(x) {
// Something errored before we could complete the shell so we emit an alternative shell.
res.statusCode = 500;
res.send('<!doctype><p>Error</p>');
},
onError(x) {
didError = true;
console.error(x);
},
});
// Abandon and switch to client rendering after 5 seconds.
// Try lowering this to see the client recover.
setTimeout(abort, 5000);
}
@@ -0,0 +1,12 @@
import React from 'react';
import Chrome from './Chrome';
import Page from './Page';
export default function App({assets}) {
return (
<Chrome title="Hello World" assets={assets}>
<Page />
</Chrome>
);
}
@@ -0,0 +1,5 @@
body {
margin: 10px;
padding: 0;
font-family: sans-serif;
}
@@ -0,0 +1,33 @@
import React, {Component} from 'react';
import './Chrome.css';
export default class Chrome extends Component {
render() {
const assets = this.props.assets;
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" href="favicon.ico" />
<link rel="stylesheet" href={assets['main.css']} />
<title>{this.props.title}</title>
</head>
<body>
<noscript
dangerouslySetInnerHTML={{
__html: `<b>Enable JavaScript to run this app.</b>`,
}}
/>
{this.props.children}
<script
dangerouslySetInnerHTML={{
__html: `assetManifest = ${JSON.stringify(assets)};`,
}}
/>
</body>
</html>
);
}
}
@@ -0,0 +1,79 @@
import React, {
unstable_ViewTransition as ViewTransition,
startTransition,
useEffect,
useState,
unstable_Activity as Activity,
} from 'react';
import './Page.css';
const a = (
<div key="a">
<ViewTransition>
<div>a</div>
</ViewTransition>
</div>
);
const b = (
<div key="b">
<ViewTransition>
<div>b</div>
</ViewTransition>
</div>
);
export default function Page() {
const [show, setShow] = useState(false);
useEffect(() => {
startTransition(() => {
setShow(true);
});
}, []);
const exclamation = (
<ViewTransition name="exclamation">
<span>!</span>
</ViewTransition>
);
return (
<div>
<button
onClick={() => {
startTransition(() => {
setShow(show => !show);
});
}}>
{show ? 'A' : 'B'}
</button>
<ViewTransition>
<div>
{show ? (
<div>
{a}
{b}
</div>
) : (
<div>
{b}
{a}
</div>
)}
<ViewTransition>
{show ? <div>hello{exclamation}</div> : <section>Loading</section>}
</ViewTransition>
{show ? null : (
<ViewTransition>
<div>world{exclamation}</div>
</ViewTransition>
)}
<Activity mode={show ? 'visible' : 'hidden'}>
<ViewTransition>
<div>!!</div>
</ViewTransition>
</Activity>
</div>
</ViewTransition>
</div>
);
}
+6
View File
@@ -0,0 +1,6 @@
import React from 'react';
import {hydrateRoot} from 'react-dom/client';
import App from './components/App';
hydrateRoot(document, <App assets={window.assetManifest} />);
File diff suppressed because it is too large Load Diff
+45
View File
@@ -455,6 +455,51 @@ export function unhideTextInstance(textInstance, text): void {
// Noop
}
export function applyViewTransitionName(instance, name) {
// Noop
}
export function restoreViewTransitionName(instance, props) {
// Noop
}
export function cancelViewTransitionName(instance, name, props) {
// Noop
}
export function cancelRootViewTransitionName(rootContainer) {
// Noop
}
export function restoreRootViewTransitionName(rootContainer) {
// Noop
}
export type InstanceMeasurement = null;
export function measureInstance(instance) {
return null;
}
export function wasInstanceInViewport(measurement): boolean {
return true;
}
export function hasInstanceChanged(oldMeasurement, newMeasurement): boolean {
return false;
}
export function hasInstanceAffectedParent(
oldMeasurement,
newMeasurement,
): boolean {
return false;
}
export function startViewTransition() {
return false;
}
export function clearContainer(container) {
// TODO Implement this
}
+4
View File
@@ -24,6 +24,7 @@ import {
REACT_SUSPENSE_LIST_TYPE,
REACT_SUSPENSE_TYPE,
REACT_TRACING_MARKER_TYPE,
REACT_VIEW_TRANSITION_TYPE,
} from 'shared/ReactSymbols';
import {enableRenderableContext} from 'shared/ReactFeatureFlags';
import {
@@ -678,6 +679,7 @@ function typeOfWithLegacyElementSymbol(object: any): mixed {
case REACT_STRICT_MODE_TYPE:
case REACT_SUSPENSE_TYPE:
case REACT_SUSPENSE_LIST_TYPE:
case REACT_VIEW_TRANSITION_TYPE:
return type;
default:
const $$typeofType = type && type.$$typeof;
@@ -739,6 +741,8 @@ export function getDisplayNameForReactElement(
return 'Suspense';
case REACT_SUSPENSE_LIST_TYPE:
return 'SuspenseList';
case REACT_VIEW_TRANSITION_TYPE:
return 'ViewTransition';
case REACT_TRACING_MARKER_TYPE:
return 'TracingMarker';
default:
@@ -11,6 +11,7 @@ import hyphenateStyleName from '../shared/hyphenateStyleName';
import warnValidStyle from '../shared/warnValidStyle';
import isUnitlessNumber from '../shared/isUnitlessNumber';
import {checkCSSPropertyStringCoercion} from 'shared/CheckStringCoercion';
import {trackHostMutation} from 'react-reconciler/src/ReactFiberMutationTracking';
/**
* Operations for dealing with CSS properties.
@@ -144,12 +145,14 @@ export function setValueForStyles(node, styles, prevStyles) {
} else {
style[styleName] = '';
}
trackHostMutation();
}
}
for (const styleName in styles) {
const value = styles[styleName];
if (styles.hasOwnProperty(styleName) && prevStyles[styleName] !== value) {
setValueForStyle(style, styleName, value);
trackHostMutation();
}
}
} else {
@@ -11,6 +11,7 @@ import isAttributeNameSafe from '../shared/isAttributeNameSafe';
import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags';
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';
import {trackHostMutation} from 'react-reconciler/src/ReactFiberMutationTracking';
/**
* Get the value for a attribute on a node. Only used in DEV for SSR validation.
@@ -217,6 +218,8 @@ export function setValueForPropertyOnCustomComponent(
}
}
trackHostMutation();
if (name in (node: any)) {
(node: any)[name] = value;
return;
+64 -12
View File
@@ -63,6 +63,8 @@ import {validateProperties as validateInputProperties} from '../shared/ReactDOMN
import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
import sanitizeURL from '../shared/sanitizeURL';
import {trackHostMutation} from 'react-reconciler/src/ReactFiberMutationTracking';
import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags';
import {
mediaEventTypes,
@@ -363,6 +365,8 @@ function setProp(
// $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
setTextContent(domElement, '' + value);
}
} else {
return;
}
break;
}
@@ -386,7 +390,7 @@ function setProp(
}
case 'style': {
setValueForStyles(domElement, value, prevValue);
break;
return;
}
// These attributes accept URLs. These must not allow javascript: URLS.
case 'data':
@@ -524,7 +528,7 @@ function setProp(
}
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
}
break;
return;
}
case 'onScroll': {
if (value != null) {
@@ -533,7 +537,7 @@ function setProp(
}
listenToNonDelegatedEvent('scroll', domElement);
}
break;
return;
}
case 'onScrollEnd': {
if (value != null) {
@@ -542,7 +546,7 @@ function setProp(
}
listenToNonDelegatedEvent('scrollend', domElement);
}
break;
return;
}
case 'dangerouslySetInnerHTML': {
if (value != null) {
@@ -849,7 +853,7 @@ function setProp(
}
case 'innerText':
case 'textContent':
break;
return;
case 'popoverTarget':
if (__DEV__) {
if (
@@ -879,12 +883,16 @@ function setProp(
) {
warnForInvalidEventListener(key, value);
}
// Updating events doesn't affect the visuals.
return;
} else {
const attributeName = getAttributeAlias(key);
setValueForAttribute(domElement, attributeName, value);
}
}
}
// To avoid marking things as host mutations we do early returns above.
trackHostMutation();
}
function setPropOnCustomElement(
@@ -898,7 +906,7 @@ function setPropOnCustomElement(
switch (key) {
case 'style': {
setValueForStyles(domElement, value, prevValue);
break;
return;
}
case 'dangerouslySetInnerHTML': {
if (value != null) {
@@ -927,6 +935,8 @@ function setPropOnCustomElement(
} else if (typeof value === 'number' || typeof value === 'bigint') {
// $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
setTextContent(domElement, '' + value);
} else {
return;
}
break;
}
@@ -937,7 +947,7 @@ function setPropOnCustomElement(
}
listenToNonDelegatedEvent('scroll', domElement);
}
break;
return;
}
case 'onScrollEnd': {
if (value != null) {
@@ -946,7 +956,7 @@ function setPropOnCustomElement(
}
listenToNonDelegatedEvent('scrollend', domElement);
}
break;
return;
}
case 'onClick': {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
@@ -956,29 +966,34 @@ function setPropOnCustomElement(
}
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
}
break;
return;
}
case 'suppressContentEditableWarning':
case 'suppressHydrationWarning':
case 'innerHTML':
case 'ref': {
// Noop
break;
return;
}
case 'innerText': // Properties
case 'textContent':
break;
return;
// Fall through
default: {
if (registrationNameDependencies.hasOwnProperty(key)) {
if (__DEV__ && value != null && typeof value !== 'function') {
warnForInvalidEventListener(key, value);
}
return;
} else {
setValueForPropertyOnCustomComponent(domElement, key, value);
// We track mutations inside this call.
return;
}
}
}
// To avoid marking things as host mutations we do early returns above.
trackHostMutation();
}
export function setInitialProperties(
@@ -1430,26 +1445,44 @@ export function updateProperties(
) {
switch (propKey) {
case 'type': {
if (nextProp !== lastProp) {
trackHostMutation();
}
type = nextProp;
break;
}
case 'name': {
if (nextProp !== lastProp) {
trackHostMutation();
}
name = nextProp;
break;
}
case 'checked': {
if (nextProp !== lastProp) {
trackHostMutation();
}
checked = nextProp;
break;
}
case 'defaultChecked': {
if (nextProp !== lastProp) {
trackHostMutation();
}
defaultChecked = nextProp;
break;
}
case 'value': {
if (nextProp !== lastProp) {
trackHostMutation();
}
value = nextProp;
break;
}
case 'defaultValue': {
if (nextProp !== lastProp) {
trackHostMutation();
}
defaultValue = nextProp;
break;
}
@@ -1553,8 +1586,9 @@ export function updateProperties(
}
// Fallthrough
default: {
if (!nextProps.hasOwnProperty(propKey))
if (!nextProps.hasOwnProperty(propKey)) {
setProp(domElement, tag, propKey, null, nextProps, lastProp);
}
}
}
}
@@ -1568,15 +1602,24 @@ export function updateProperties(
) {
switch (propKey) {
case 'value': {
if (nextProp !== lastProp) {
trackHostMutation();
}
value = nextProp;
// This is handled by updateSelect below.
break;
}
case 'defaultValue': {
if (nextProp !== lastProp) {
trackHostMutation();
}
defaultValue = nextProp;
break;
}
case 'multiple': {
if (nextProp !== lastProp) {
trackHostMutation();
}
multiple = nextProp;
// TODO: Just move the special case in here from setProp.
}
@@ -1635,11 +1678,17 @@ export function updateProperties(
) {
switch (propKey) {
case 'value': {
if (nextProp !== lastProp) {
trackHostMutation();
}
value = nextProp;
// This is handled by updateTextarea below.
break;
}
case 'defaultValue': {
if (nextProp !== lastProp) {
trackHostMutation();
}
defaultValue = nextProp;
break;
}
@@ -1703,6 +1752,9 @@ export function updateProperties(
) {
switch (propKey) {
case 'selected': {
if (nextProp !== lastProp) {
trackHostMutation();
}
// TODO: Remove support for selected on option.
(domElement: any).selected =
nextProp &&
+225 -1
View File
@@ -120,7 +120,12 @@ export type Props = {
hidden?: boolean,
suppressHydrationWarning?: boolean,
dangerouslySetInnerHTML?: mixed,
style?: {display?: string, ...},
style?: {
display?: string,
viewTransitionName?: string,
'view-transition-name'?: string,
...
},
bottom?: null | number,
left?: null | number,
right?: null | number,
@@ -149,6 +154,7 @@ export type EventTargetChildElement = {
},
...
};
export type Container =
| interface extends Element {_reactRootContainer?: FiberRoot}
| interface extends Document {_reactRootContainer?: FiberRoot}
@@ -978,6 +984,224 @@ export function unhideTextInstance(
textInstance.nodeValue = text;
}
export function applyViewTransitionName(
instance: Instance,
name: string,
): void {
instance = ((instance: any): HTMLElement);
// $FlowFixMe[prop-missing]
instance.style.viewTransitionName = name;
}
export function restoreViewTransitionName(
instance: Instance,
props: Props,
): void {
instance = ((instance: any): HTMLElement);
const styleProp = props[STYLE];
const viewTransitionName =
styleProp !== undefined && styleProp !== null
? styleProp.hasOwnProperty('viewTransitionName')
? styleProp.viewTransitionName
: styleProp.hasOwnProperty('view-transition-name')
? styleProp['view-transition-name']
: null
: null;
// $FlowFixMe[prop-missing]
instance.style.viewTransitionName =
viewTransitionName == null || typeof viewTransitionName === 'boolean'
? ''
: // The value would've errored already if it wasn't safe.
// eslint-disable-next-line react-internal/safe-string-coercion
('' + viewTransitionName).trim();
}
export function cancelViewTransitionName(
instance: Instance,
oldName: string,
props: Props,
): void {
// To cancel the "new" state and paint this instance as part of the parent, all we have to do
// is remove the view-transition-name before we exit startViewTransition.
restoreViewTransitionName(instance, props);
// There isn't a way to cancel an "old" state but what we can do is hide it by animating it.
// Since it is already removed from the old state of the parent, this technique only works
// if the parent also isn't transitioning. Therefore we should only cancel the root most
// ViewTransitions.
const documentElement = instance.ownerDocument.documentElement;
if (documentElement !== null) {
documentElement.animate(
{opacity: [0, 0], pointerEvents: ['none', 'none']},
{
duration: 0,
fill: 'forwards',
pseudoElement: '::view-transition-group(' + oldName + ')',
},
);
}
}
export function cancelRootViewTransitionName(rootContainer: Container): void {
const documentElement: null | HTMLElement =
rootContainer.nodeType === DOCUMENT_NODE
? (rootContainer: any).documentElement
: rootContainer.ownerDocument.documentElement;
if (
documentElement !== null &&
// $FlowFixMe[prop-missing]
documentElement.style.viewTransitionName === ''
) {
// $FlowFixMe[prop-missing]
documentElement.style.viewTransitionName = 'none';
documentElement.animate(
{opacity: [0, 0], pointerEvents: ['none', 'none']},
{
duration: 0,
fill: 'forwards',
pseudoElement: '::view-transition-group(root)',
},
);
// By default the root ::view-transition selector captures all pointer events,
// which means nothing gets interactive. We want to let whatever is not animating
// remain interactive during the transition. To do that, we set the size to nothing
// so that the transition doesn't capture any clicks. We don't set pointer-events
// on this one as that would apply to all running transitions. This lets animations
// that are running to block clicks so that they don't end up incorrectly hitting
// whatever is below the animation.
documentElement.animate(
{width: [0, 0], height: [0, 0]},
{
duration: 0,
fill: 'forwards',
pseudoElement: '::view-transition',
},
);
}
}
export function restoreRootViewTransitionName(rootContainer: Container): void {
const documentElement: null | HTMLElement =
rootContainer.nodeType === DOCUMENT_NODE
? (rootContainer: any).documentElement
: rootContainer.ownerDocument.documentElement;
if (
documentElement !== null &&
// $FlowFixMe[prop-missing]
documentElement.style.viewTransitionName === 'none'
) {
// $FlowFixMe[prop-missing]
documentElement.style.viewTransitionName = '';
}
}
export type InstanceMeasurement = {
rect: ClientRect | DOMRect,
abs: boolean, // is absolutely positioned
clip: boolean, // is a clipping parent
view: boolean, // is in viewport bounds
};
export function measureInstance(instance: Instance): InstanceMeasurement {
const ownerWindow = instance.ownerDocument.defaultView;
const rect = instance.getBoundingClientRect();
const computedStyle = getComputedStyle(instance);
return {
rect: rect,
abs:
// Absolutely positioned instances don't contribute their size to the parent.
computedStyle.position === 'absolute' ||
computedStyle.position === 'fixed',
clip:
// If a ViewTransition boundary acts as a clipping parent group we should
// always mark it to animate if its children do so that we can clip them.
// This doesn't actually have any effect yet until browsers implement
// layered capture and nested view transitions.
computedStyle.clipPath !== 'none' ||
computedStyle.overflow !== 'visible' ||
computedStyle.filter !== 'none' ||
computedStyle.mask !== 'none' ||
computedStyle.mask !== 'none' ||
computedStyle.borderRadius !== '0px',
view:
// If the instance was within the bounds of the viewport. We don't care as
// much about if it was fully occluded because then it can still pop out.
rect.bottom >= 0 &&
rect.right >= 0 &&
rect.top <= ownerWindow.innerHeight &&
rect.left <= ownerWindow.innerWidth,
};
}
export function wasInstanceInViewport(
measurement: InstanceMeasurement,
): boolean {
return measurement.view;
}
export function hasInstanceChanged(
oldMeasurement: InstanceMeasurement,
newMeasurement: InstanceMeasurement,
): boolean {
// Note: This is not guaranteed from the same instance in the case that the Instance of the
// ViewTransition swaps out but it's still the same ViewTransition instance.
if (newMeasurement.clip) {
// If we're a clipping parent, we always animate if any of our children do so that we can clip
// them. This doesn't yet until browsers implement layered capture and nested view transitions.
return true;
}
const oldRect = oldMeasurement.rect;
const newRect = newMeasurement.rect;
return (
oldRect.y !== newRect.y ||
oldRect.x !== newRect.x ||
oldRect.height !== newRect.height ||
oldRect.width !== newRect.width
);
}
export function hasInstanceAffectedParent(
oldMeasurement: InstanceMeasurement,
newMeasurement: InstanceMeasurement,
): boolean {
// Note: This is not guaranteed from the same instance in the case that the Instance of the
// ViewTransition swaps out but it's still the same ViewTransition instance.
// If the instance has resized, it might have affected the parent layout.
if (newMeasurement.abs) {
// Absolutely positioned elements don't affect the parent layout, unless they
// previously were not absolutely positioned.
return !oldMeasurement.abs;
}
const oldRect = oldMeasurement.rect;
const newRect = newMeasurement.rect;
return oldRect.height !== newRect.height || oldRect.width !== newRect.width;
}
export function startViewTransition(
rootContainer: Container,
mutationCallback: () => void,
afterMutationCallback: () => void,
layoutCallback: () => void,
passiveCallback: () => mixed,
): boolean {
const ownerDocument =
rootContainer.nodeType === DOCUMENT_NODE
? rootContainer
: rootContainer.ownerDocument;
// $FlowFixMe[prop-missing]
if (typeof ownerDocument.startViewTransition !== 'function') {
return false;
}
// $FlowFixMe[incompatible-use]
const transition = ownerDocument.startViewTransition(() => {
mutationCallback();
// TODO: Wait for fonts.
afterMutationCallback();
});
transition.ready.then(layoutCallback, layoutCallback);
transition.finished.then(passiveCallback);
return true;
}
export function clearContainer(container: Container): void {
const nodeType = container.nodeType;
if (nodeType === DOCUMENT_NODE) {
+2
View File
@@ -23,6 +23,7 @@ import {
REACT_STRICT_MODE_TYPE,
REACT_SUSPENSE_TYPE,
REACT_SUSPENSE_LIST_TYPE,
REACT_VIEW_TRANSITION_TYPE,
} from 'shared/ReactSymbols';
import isValidElementType from 'shared/isValidElementType';
import {enableRenderableContext} from 'shared/ReactFeatureFlags';
@@ -40,6 +41,7 @@ export function typeOf(object: any): mixed {
case REACT_STRICT_MODE_TYPE:
case REACT_SUSPENSE_TYPE:
case REACT_SUSPENSE_LIST_TYPE:
case REACT_VIEW_TRANSITION_TYPE:
return type;
default:
const $$typeofType = type && type.$$typeof;
@@ -522,6 +522,73 @@ export function unhideInstance(instance: Instance, props: Props): void {
);
}
export function applyViewTransitionName(
instance: Instance,
name: string,
): void {
// Not yet implemented
}
export function restoreViewTransitionName(
instance: Instance,
props: Props,
): void {
// Not yet implemented
}
export function cancelViewTransitionName(
instance: Instance,
name: string,
props: Props,
): void {
// Not yet implemented
}
export function cancelRootViewTransitionName(rootContainer: Container): void {
// Not yet implemented
}
export function restoreRootViewTransitionName(rootContainer: Container): void {
// Not yet implemented
}
export type InstanceMeasurement = null;
export function measureInstance(instance: Instance): InstanceMeasurement {
// This heuristic is better implemented at the native layer.
return null;
}
export function wasInstanceInViewport(
measurement: InstanceMeasurement,
): boolean {
return true;
}
export function hasInstanceChanged(
oldMeasurement: InstanceMeasurement,
newMeasurement: InstanceMeasurement,
): boolean {
return false;
}
export function hasInstanceAffectedParent(
oldMeasurement: InstanceMeasurement,
newMeasurement: InstanceMeasurement,
): boolean {
return false;
}
export function startViewTransition(
rootContainer: Container,
mutationCallback: () => void,
afterMutationCallback: () => void,
layoutCallback: () => void,
passiveCallback: () => mixed,
): boolean {
return false;
}
export function clearContainer(container: Container): void {
// TODO Implement this for React Native
// UIManager does not expose a "remove all" type method.
+47
View File
@@ -81,6 +81,7 @@ type CreateRootOptions = {
onCaughtError?: (error: mixed, errorInfo: {componentStack: string}) => void,
...
};
type InstanceMeasurement = null;
type SuspenseyCommitSubscription = {
pendingCount: number,
@@ -731,6 +732,52 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
textInstance.hidden = false;
},
applyViewTransitionName(instance: Instance, name: string): void {},
restoreViewTransitionName(instance: Instance, props: Props): void {},
cancelViewTransitionName(
instance: Instance,
name: string,
props: Props,
): void {},
cancelRootViewTransitionName(rootContainer: Container): void {},
restoreRootViewTransitionName(rootContainer: Container): void {},
measureInstance(instance: Instance): InstanceMeasurement {
return null;
},
wasInstanceInViewport(measurement: InstanceMeasurement): boolean {
return true;
},
hasInstanceChanged(
oldMeasurement: InstanceMeasurement,
newMeasurement: InstanceMeasurement,
): boolean {
return false;
},
hasInstanceAffectedParent(
oldMeasurement: InstanceMeasurement,
newMeasurement: InstanceMeasurement,
): boolean {
return false;
},
startViewTransition(
rootContainer: Container,
mutationCallback: () => void,
afterMutationCallback: () => void,
layoutCallback: () => void,
passiveCallback: () => mixed,
): boolean {
return false;
},
resetTextContent(instance: Instance): void {
instance.text = null;
},
+29
View File
@@ -19,6 +19,10 @@ import type {
OffscreenProps,
OffscreenInstance,
} from './ReactFiberActivityComponent';
import type {
ViewTransitionProps,
ViewTransitionInstance,
} from './ReactFiberViewTransitionComponent';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent';
import {
@@ -37,6 +41,7 @@ import {
disableLegacyMode,
enableObjectFiber,
enableOwnerStacks,
enableViewTransition,
} from 'shared/ReactFeatureFlags';
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
import {ConcurrentRoot} from './ReactRootTags';
@@ -66,6 +71,7 @@ import {
LegacyHiddenComponent,
TracingMarkerComponent,
Throw,
ViewTransitionComponent,
} from './ReactWorkTags';
import {OffscreenVisible} from './ReactFiberActivityComponent';
import {getComponentNameFromOwner} from 'react-reconciler/src/getComponentNameFromFiber';
@@ -101,6 +107,7 @@ import {
REACT_LEGACY_HIDDEN_TYPE,
REACT_TRACING_MARKER_TYPE,
REACT_ELEMENT_TYPE,
REACT_VIEW_TRANSITION_TYPE,
} from 'shared/ReactSymbols';
import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent';
import {
@@ -617,6 +624,11 @@ export function createFiberFromTypeAndProps(
return createFiberFromLegacyHidden(pendingProps, mode, lanes, key);
}
// Fall through
case REACT_VIEW_TRANSITION_TYPE:
if (enableViewTransition) {
return createFiberFromViewTransition(pendingProps, mode, lanes, key);
}
// Fall through
case REACT_SCOPE_TYPE:
if (enableScopeAPI) {
return createFiberFromScope(type, pendingProps, mode, lanes, key);
@@ -863,6 +875,23 @@ export function createFiberFromOffscreen(
return fiber;
}
export function createFiberFromViewTransition(
pendingProps: ViewTransitionProps,
mode: TypeOfMode,
lanes: Lanes,
key: null | string,
): Fiber {
const fiber = createFiber(ViewTransitionComponent, pendingProps, key, mode);
fiber.elementType = REACT_VIEW_TRANSITION_TYPE;
fiber.lanes = lanes;
const instance: ViewTransitionInstance = {
autoName: null,
paired: null,
};
fiber.stateNode = instance;
return fiber;
}
export function createFiberFromLegacyHidden(
pendingProps: OffscreenProps,
mode: TypeOfMode,
+44
View File
@@ -28,6 +28,11 @@ import type {
OffscreenQueue,
OffscreenInstance,
} from './ReactFiberActivityComponent';
import type {
ViewTransitionProps,
ViewTransitionInstance,
} from './ReactFiberViewTransitionComponent';
import {assignViewTransitionAutoName} from './ReactFiberViewTransitionComponent';
import {OffscreenDetached} from './ReactFiberActivityComponent';
import type {
Cache,
@@ -71,6 +76,7 @@ import {
CacheComponent,
TracingMarkerComponent,
Throw,
ViewTransitionComponent,
} from './ReactWorkTags';
import {
NoFlags,
@@ -90,6 +96,7 @@ import {
ForceClientRender,
Passive,
DidDefer,
ViewTransitionNamedStatic,
} from './ReactFiberFlags';
import {
disableLegacyContext,
@@ -107,6 +114,7 @@ import {
disableDefaultPropsExceptForClasses,
enableOwnerStacks,
enableHydrationLaneScheduling,
enableViewTransition,
} from 'shared/ReactFeatureFlags';
import isArray from 'shared/isArray';
import shallowEqual from 'shared/shallowEqual';
@@ -257,6 +265,7 @@ import {
markSkippedUpdateLanes,
getWorkInProgressRoot,
peekDeferredLane,
trackAppearingViewTransition,
} from './ReactFiberWorkLoop';
import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates';
import {pushCacheProvider, CacheContext} from './ReactFiberCacheComponent';
@@ -3231,6 +3240,35 @@ function updateSuspenseListComponent(
return workInProgress.child;
}
function updateViewTransition(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const pendingProps: ViewTransitionProps = workInProgress.pendingProps;
const instance: ViewTransitionInstance = workInProgress.stateNode;
if (pendingProps.name != null && pendingProps.name !== 'auto') {
// Explicitly named boundary. We track it so that we can pair it up with another explicit
// boundary if we get deleted.
workInProgress.flags |= ViewTransitionNamedStatic;
if (current === null) {
// This is a new mount. We track it in case we end up having a deletion with the same name.
// TODO: A problem with this strategy is that this subtree might not actually end up mounted.
trackAppearingViewTransition(instance, pendingProps.name);
}
} else {
// Assign an auto generated name using the useId algorthim if an explicit one is not provided.
// We don't need the name yet but we do it here to allow hydration state to be used.
// We might end up needing these to line up if we want to Transition from dehydrated fallback
// to client rendered content. If we don't end up using that we could just assign an incremeting
// counter in the commit phase instead.
assignViewTransitionAutoName(pendingProps, instance);
}
const nextChildren = pendingProps.children;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function updatePortalComponent(
current: Fiber | null,
workInProgress: Fiber,
@@ -4015,6 +4053,12 @@ function beginWork(
}
break;
}
case ViewTransitionComponent: {
if (enableViewTransition) {
return updateViewTransition(current, workInProgress, renderLanes);
}
break;
}
case Throw: {
// This represents a Component that threw in the reconciliation phase.
// So we'll rethrow here. This might be a Thenable.
@@ -51,6 +51,7 @@ import {
acquireSingletonInstance,
} from './ReactFiberConfig';
import {captureCommitPhaseError} from './ReactFiberWorkLoop';
import {trackHostMutation} from './ReactFiberMutationTracking';
import {runWithFiberInDEV} from './ReactCurrentFiber';
@@ -80,7 +81,7 @@ export function commitHostUpdate(
finishedWork: Fiber,
newProps: any,
oldProps: any,
) {
): void {
try {
if (__DEV__) {
runWithFiberInDEV(
@@ -101,6 +102,7 @@ export function commitHostUpdate(
finishedWork,
);
}
// Mutations are tracked manually from within commitUpdate.
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
@@ -124,6 +126,7 @@ export function commitHostTextUpdate(
} else {
commitTextUpdate(textInstance, oldText, newText);
}
trackHostMutation();
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
@@ -137,6 +140,7 @@ export function commitHostResetTextContent(finishedWork: Fiber) {
} else {
resetTextContent(instance);
}
trackHostMutation();
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
@@ -281,6 +285,7 @@ function insertOrAppendPlacementNodeIntoContainer(
} else {
appendChildToContainer(parent, stateNode);
}
trackHostMutation();
} else if (
tag === HostPortal ||
(supportsSingletons ? tag === HostSingleton : false)
@@ -316,6 +321,7 @@ function insertOrAppendPlacementNode(
} else {
appendChild(parent, stateNode);
}
trackHostMutation();
} else if (
tag === HostPortal ||
(supportsSingletons ? tag === HostSingleton : false)
@@ -424,6 +430,7 @@ export function commitHostRemoveChildFromContainer(
} else {
removeChildFromContainer(parentContainer, hostInstance);
}
trackHostMutation();
} catch (error) {
captureCommitPhaseError(deletedFiber, nearestMountedAncestor, error);
}
@@ -446,6 +453,7 @@ export function commitHostRemoveChild(
} else {
removeChild(parentInstance, hostInstance);
}
trackHostMutation();
} catch (error) {
captureCommitPhaseError(deletedFiber, nearestMountedAncestor, error);
}
@@ -468,6 +476,7 @@ export function commitHostRootContainerChildren(
} else {
replaceContainerChildren(containerInfo, pendingChildren);
}
trackHostMutation();
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
File diff suppressed because it is too large Load Diff
+75
View File
@@ -28,6 +28,10 @@ import type {
OffscreenState,
OffscreenQueue,
} from './ReactFiberActivityComponent';
import type {
ViewTransitionProps,
ViewTransitionInstance,
} from './ReactFiberViewTransitionComponent';
import {isOffscreenManual} from './ReactFiberActivityComponent';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent';
import type {Cache} from './ReactFiberCacheComponent';
@@ -42,6 +46,7 @@ import {
passChildrenWhenCloningPersistedNodes,
disableLegacyMode,
enableSiblingPrerendering,
enableViewTransition,
} from 'shared/ReactFeatureFlags';
import {now} from './Scheduler';
@@ -74,6 +79,7 @@ import {
CacheComponent,
TracingMarkerComponent,
Throw,
ViewTransitionComponent,
} from './ReactWorkTags';
import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
import {
@@ -92,6 +98,8 @@ import {
ScheduleRetry,
ShouldSuspendCommit,
Cloned,
ViewTransitionStatic,
ViewTransitionNamedStatic,
} from './ReactFiberFlags';
import {
@@ -156,6 +164,7 @@ import {
getWorkInProgressTransitions,
shouldRemainOnPreviousScreen,
markSpawnedRetryLane,
trackAppearingViewTransition,
} from './ReactFiberWorkLoop';
import {
OffscreenLane,
@@ -938,6 +947,34 @@ function completeDehydratedSuspenseBoundary(
}
}
function trackReappearingViewTransitions(workInProgress: Fiber): void {
if ((workInProgress.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
// This has no named view transitions in its subtree.
return;
}
// This needs to search for any explicitly named reappearing View Transitions,
// whether they were updated in this transition or unchanged from before.
let child = workInProgress.child;
while (child !== null) {
if (child.tag === OffscreenComponent && child.memoizedState === null) {
// This tree is currently hidden so we skip it.
} else {
if (
child.tag === ViewTransitionComponent &&
(child.flags & ViewTransitionNamedStatic) !== NoFlags
) {
const props: ViewTransitionProps = child.memoizedProps;
if (props.name != null && props.name !== 'auto') {
const instance: ViewTransitionInstance = child.stateNode;
trackAppearingViewTransition(instance, props.name);
}
}
trackReappearingViewTransitions(child);
}
child = child.sibling;
}
}
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
@@ -1185,6 +1222,11 @@ function completeWork(
// This can happen when we abort work.
bubbleProperties(workInProgress);
if (enableViewTransition) {
// Host Components act as their own View Transitions which doesn't run enter/exit animations.
// We clear any ViewTransitionStatic flag bubbled from inner View Transitions.
workInProgress.subtreeFlags &= ~ViewTransitionStatic;
}
return null;
}
@@ -1210,6 +1252,11 @@ function completeWork(
}
}
bubbleProperties(workInProgress);
if (enableViewTransition) {
// Host Components act as their own View Transitions which doesn't run enter/exit animations.
// We clear any ViewTransitionStatic flag bubbled from inner View Transitions.
workInProgress.subtreeFlags &= ~ViewTransitionStatic;
}
return null;
}
// Fall through
@@ -1236,6 +1283,11 @@ function completeWork(
// This can happen when we abort work.
bubbleProperties(workInProgress);
if (enableViewTransition) {
// Host Components act as their own View Transitions which doesn't run enter/exit animations.
// We clear any ViewTransitionStatic flag bubbled from inner View Transitions.
workInProgress.subtreeFlags &= ~ViewTransitionStatic;
}
return null;
}
@@ -1280,6 +1332,11 @@ function completeWork(
}
}
bubbleProperties(workInProgress);
if (enableViewTransition) {
// Host Components act as their own View Transitions which doesn't run enter/exit animations.
// We clear any ViewTransitionStatic flag bubbled from inner View Transitions.
workInProgress.subtreeFlags &= ~ViewTransitionStatic;
}
// This must come at the very end of the complete phase, because it might
// throw to suspend, and if the resource immediately loads, the work loop
@@ -1739,6 +1796,14 @@ function completeWork(
const prevIsHidden = prevState !== null;
if (prevIsHidden !== nextIsHidden) {
workInProgress.flags |= Visibility;
if (enableViewTransition && !nextIsHidden) {
// If we're revealing a new tree, we need to find any named
// ViewTransitions inside it that might have a deleted pair.
// We do this in the complete phase in case the tree has
// changed during the reveal but we have to do it before we
// find the first deleted pair in the before mutation phase.
trackReappearingViewTransitions(workInProgress);
}
}
} else {
// On initial mount, we only need a Visibility effect if the tree
@@ -1832,6 +1897,16 @@ function completeWork(
}
return null;
}
case ViewTransitionComponent: {
if (enableViewTransition) {
// We're a component that might need an exit transition. This flag will
// bubble up to the parent tree to indicate that there's a child that
// might need an exit View Transition upon unmount.
workInProgress.flags |= ViewTransitionStatic;
bubbleProperties(workInProgress);
}
return null;
}
case Throw: {
if (!disableLegacyMode) {
// Only Legacy Mode completes an errored node.
@@ -35,3 +35,14 @@ export const hideTextInstance = shim;
export const unhideInstance = shim;
export const unhideTextInstance = shim;
export const clearContainer = shim;
export const applyViewTransitionName = shim;
export const restoreViewTransitionName = shim;
export const cancelViewTransitionName = shim;
export const cancelRootViewTransitionName = shim;
export const restoreRootViewTransitionName = shim;
export type InstanceMeasurement = null;
export const measureInstance = shim;
export const wasInstanceInViewport = shim;
export const hasInstanceChanged = shim;
export const hasInstanceAffectedParent = shim;
export const startViewTransition = shim;
+55 -32
View File
@@ -15,29 +15,29 @@ import {
export type Flags = number;
// Don't change these values. They're used by React Dev Tools.
export const NoFlags = /* */ 0b0000000000000000000000000000;
export const PerformedWork = /* */ 0b0000000000000000000000000001;
export const Placement = /* */ 0b0000000000000000000000000010;
export const DidCapture = /* */ 0b0000000000000000000010000000;
export const Hydrating = /* */ 0b0000000000000001000000000000;
export const NoFlags = /* */ 0b0000000000000000000000000000000;
export const PerformedWork = /* */ 0b0000000000000000000000000000001;
export const Placement = /* */ 0b0000000000000000000000000000010;
export const DidCapture = /* */ 0b0000000000000000000000010000000;
export const Hydrating = /* */ 0b0000000000000000001000000000000;
// You can change the rest (and add more).
export const Update = /* */ 0b0000000000000000000000000100;
export const Cloned = /* */ 0b0000000000000000000000001000;
export const Update = /* */ 0b0000000000000000000000000000100;
export const Cloned = /* */ 0b0000000000000000000000000001000;
export const ChildDeletion = /* */ 0b0000000000000000000000010000;
export const ContentReset = /* */ 0b0000000000000000000000100000;
export const Callback = /* */ 0b0000000000000000000001000000;
/* Used by DidCapture: 0b0000000000000000000010000000; */
export const ChildDeletion = /* */ 0b0000000000000000000000000010000;
export const ContentReset = /* */ 0b0000000000000000000000000100000;
export const Callback = /* */ 0b0000000000000000000000001000000;
/* Used by DidCapture: 0b0000000000000000000000010000000; */
export const ForceClientRender = /* */ 0b0000000000000000000100000000;
export const Ref = /* */ 0b0000000000000000001000000000;
export const Snapshot = /* */ 0b0000000000000000010000000000;
export const Passive = /* */ 0b0000000000000000100000000000;
/* Used by Hydrating: 0b0000000000000001000000000000; */
export const ForceClientRender = /* */ 0b0000000000000000000000100000000;
export const Ref = /* */ 0b0000000000000000000001000000000;
export const Snapshot = /* */ 0b0000000000000000000010000000000;
export const Passive = /* */ 0b0000000000000000000100000000000;
/* Used by Hydrating: 0b0000000000000000001000000000000; */
export const Visibility = /* */ 0b0000000000000010000000000000;
export const StoreConsistency = /* */ 0b0000000000000100000000000000;
export const Visibility = /* */ 0b0000000000000000010000000000000;
export const StoreConsistency = /* */ 0b0000000000000000100000000000000;
// It's OK to reuse these bits because these flags are mutually exclusive for
// different fiber types. We should really be doing this for as many flags as
@@ -46,35 +46,44 @@ export const ScheduleRetry = StoreConsistency;
export const ShouldSuspendCommit = Visibility;
export const DidDefer = ContentReset;
export const FormReset = Snapshot;
export const AffectedParentLayout = ContentReset;
export const LifecycleEffectMask =
Passive | Update | Callback | Ref | Snapshot | StoreConsistency;
// Union of all commit flags (flags with the lifetime of a particular commit)
export const HostEffectMask = /* */ 0b0000000000000111111111111111;
export const HostEffectMask = /* */ 0b0000000000000000111111111111111;
// These are not really side effects, but we still reuse this field.
export const Incomplete = /* */ 0b0000000000001000000000000000;
export const ShouldCapture = /* */ 0b0000000000010000000000000000;
export const ForceUpdateForLegacySuspense = /* */ 0b0000000000100000000000000000;
export const DidPropagateContext = /* */ 0b0000000001000000000000000000;
export const NeedsPropagation = /* */ 0b0000000010000000000000000000;
export const Forked = /* */ 0b0000000100000000000000000000;
export const Incomplete = /* */ 0b0000000000000001000000000000000;
export const ShouldCapture = /* */ 0b0000000000000010000000000000000;
export const ForceUpdateForLegacySuspense = /* */ 0b0000000000000100000000000000000;
export const DidPropagateContext = /* */ 0b0000000000001000000000000000000;
export const NeedsPropagation = /* */ 0b0000000000010000000000000000000;
export const Forked = /* */ 0b0000000000100000000000000000000;
// Static tags describe aspects of a fiber that are not specific to a render,
// e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
// This enables us to defer more work in the unmount case,
// since we can defer traversing the tree during layout to look for Passive effects,
// and instead rely on the static flag as a signal that there may be cleanup work.
export const LayoutStatic = /* */ 0b0000010000000000000000000000;
export const SnapshotStatic = /* */ 0b0000000001000000000000000000000;
export const LayoutStatic = /* */ 0b0000000010000000000000000000000;
export const RefStatic = LayoutStatic;
export const PassiveStatic = /* */ 0b0000100000000000000000000000;
export const MaySuspendCommit = /* */ 0b0001000000000000000000000000;
export const PassiveStatic = /* */ 0b0000000100000000000000000000000;
export const MaySuspendCommit = /* */ 0b0000001000000000000000000000000;
// ViewTransitionNamedStatic tracks explicitly name ViewTransition components deeply
// that might need to be visited during clean up. This is similar to SnapshotStatic
// if there was any other use for it.
export const ViewTransitionNamedStatic = /* */ SnapshotStatic;
// ViewTransitionStatic tracks whether there are an ViewTransition components from
// the nearest HostComponent down. It resets at every HostComponent level.
export const ViewTransitionStatic = /* */ 0b0000010000000000000000000000000;
// Flag used to identify newly inserted fibers. It isn't reset after commit unlike `Placement`.
export const PlacementDEV = /* */ 0b0010000000000000000000000000;
export const MountLayoutDev = /* */ 0b0100000000000000000000000000;
export const MountPassiveDev = /* */ 0b1000000000000000000000000000;
export const PlacementDEV = /* */ 0b0000100000000000000000000000000;
export const MountLayoutDev = /* */ 0b0001000000000000000000000000000;
export const MountPassiveDev = /* */ 0b0010000000000000000000000000000;
// Groups of flags that are used in the commit phase to skip over trees that
// don't contain effects, by checking subtreeFlags.
@@ -94,6 +103,11 @@ export const BeforeMutationMask: number =
Update
: 0);
// For View Transition support we use the snapshot phase to scan the tree for potentially
// affected ViewTransition components.
export const BeforeMutationTransitionMask: number =
Snapshot | Update | Placement | ChildDeletion | Visibility;
export const MutationMask =
Placement |
Update |
@@ -108,8 +122,17 @@ export const LayoutMask = Update | Callback | Ref | Visibility;
// TODO: Split into PassiveMountMask and PassiveUnmountMask
export const PassiveMask = Passive | Visibility | ChildDeletion;
// For View Transitions we need to visit anything we visited in the snapshot phase to
// restore the view-transition-name after committing the transition.
export const PassiveTransitionMask: number = PassiveMask | Update | Placement;
// Union of tags that don't get reset on clones.
// This allows certain concepts to persist without recalculating them,
// e.g. whether a subtree contains passive effects or portals.
export const StaticMask =
LayoutStatic | PassiveStatic | RefStatic | MaySuspendCommit;
LayoutStatic |
PassiveStatic |
RefStatic |
MaySuspendCommit |
ViewTransitionStatic |
ViewTransitionNamedStatic;
+4
View File
@@ -627,6 +627,10 @@ export function includesOnlyHydrationOrOffscreenLanes(lanes: Lanes): boolean {
return (lanes & (HydrationLanes | OffscreenLane)) === lanes;
}
export function includesOnlyViewTransitionEligibleLanes(lanes: Lanes): boolean {
return (lanes & (TransitionLanes | RetryLanes | IdleLane)) === lanes;
}
export function includesBlockingLane(lanes: Lanes): boolean {
const SyncDefaultLanes =
InputContinuousHydrationLane |
@@ -0,0 +1,33 @@
/**
* Copyright (c) Meta Platforms, Inc. and 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 {enableViewTransition} from 'shared/ReactFeatureFlags';
export let viewTransitionMutationContext: boolean = false;
export function pushMutationContext(): boolean {
if (!enableViewTransition) {
return false;
}
const prev = viewTransitionMutationContext;
viewTransitionMutationContext = false;
return prev;
}
export function popMutationContext(prev: boolean): void {
if (enableViewTransition) {
viewTransitionMutationContext = prev;
}
}
export function trackHostMutation(): void {
if (enableViewTransition) {
viewTransitionMutationContext = true;
}
}
@@ -0,0 +1,70 @@
/**
* Copyright (c) Meta Platforms, Inc. and 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 {ReactNodeList} from 'shared/ReactTypes';
import type {FiberRoot} from './ReactInternalTypes';
import {getWorkInProgressRoot} from './ReactFiberWorkLoop';
import {getIsHydrating} from './ReactFiberHydrationContext';
import {getTreeId} from './ReactFiberTreeContext';
export type ViewTransitionProps = {
name?: string,
children?: ReactNodeList,
};
export type ViewTransitionInstance = {
autoName: null | string, // the view-transition-name to use when an explicit one is not specified
paired: null | ViewTransitionInstance, // a temporary state during the commit phase if we have paired this with another instance
};
let globalClientIdCounter: number = 0;
export function assignViewTransitionAutoName(
props: ViewTransitionProps,
instance: ViewTransitionInstance,
): string {
if (instance.autoName !== null) {
return instance.autoName;
}
const root = ((getWorkInProgressRoot(): any): FiberRoot);
const identifierPrefix = root.identifierPrefix;
let name;
if (getIsHydrating()) {
const treeId = getTreeId();
// Use a captial R prefix for server-generated ids.
name = '\u00AB' + identifierPrefix + 'T' + treeId + '\u00BB';
} else {
// Use a lowercase r prefix for client-generated ids.
const globalClientId = globalClientIdCounter++;
name =
'\u00AB' +
identifierPrefix +
't' +
globalClientId.toString(32) +
'\u00BB';
}
instance.autoName = name;
return name;
}
export function getViewTransitionName(
props: ViewTransitionProps,
instance: ViewTransitionInstance,
): string {
if (props.name != null && props.name !== 'auto') {
return props.name;
}
// We should have assigned a name by now.
return (instance.autoName: any);
}
+96 -11
View File
@@ -23,6 +23,7 @@ import type {
import type {OffscreenInstance} from './ReactFiberActivityComponent';
import type {Resource} from './ReactFiberConfig';
import type {RootState} from './ReactFiberRoot';
import type {ViewTransitionInstance} from './ReactFiberViewTransitionComponent';
import {
enableCreateEventHandleAPI,
@@ -41,6 +42,7 @@ import {
enableComponentPerformanceTrack,
enableYieldingBeforePassive,
enableThrottledScheduling,
enableViewTransition,
} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import is from 'shared/objectIs';
@@ -91,6 +93,7 @@ import {
getCurrentUpdatePriority,
resolveUpdatePriority,
trackSchedulerEvent,
startViewTransition,
} from './ReactFiberConfig';
import {createWorkInProgress, resetWorkInProgress} from './ReactFiber';
@@ -138,6 +141,7 @@ import {
ShouldSuspendCommit,
MaySuspendCommit,
ScheduleRetry,
PassiveTransitionMask,
} from './ReactFiberFlags';
import {
NoLanes,
@@ -173,6 +177,7 @@ import {
UpdateLanes,
claimNextTransitionLane,
checkIfRootIsPrerendering,
includesOnlyViewTransitionEligibleLanes,
} from './ReactFiberLane';
import {
DiscreteEventPriority,
@@ -198,6 +203,7 @@ import {
import {
commitBeforeMutationEffects,
shouldFireAfterActiveInstanceBlur,
commitAfterMutationEffects,
commitLayoutEffects,
commitMutationEffects,
commitPassiveMountEffects,
@@ -211,6 +217,7 @@ import {
invokeLayoutEffectUnmountInDEV,
invokePassiveEffectUnmountInDEV,
accumulateSuspenseyCommit,
shouldStartViewTransition,
} from './ReactFiberCommitWork';
import {enqueueUpdate} from './ReactFiberClassUpdateQueue';
import {resetContextDependencies} from './ReactFiberNewContext';
@@ -419,6 +426,12 @@ let workInProgressRootConcurrentErrors: Array<CapturedValue<mixed>> | null =
// We will log them once the tree commits.
let workInProgressRootRecoverableErrors: Array<CapturedValue<mixed>> | null =
null;
// This tracks named ViewTransition components that might need to find deleted
// pairs in the snapshot phase.
let workInProgressAppearingViewTransitions: Map<
string,
ViewTransitionInstance,
> | null = null;
// Tracks when an update occurs during the render phase.
let workInProgressRootDidIncludeRecursiveRenderUpdate: boolean = false;
@@ -623,9 +636,10 @@ const THROTTLED_COMMIT = 2;
const NO_PENDING_EFFECTS = 0;
const PENDING_MUTATION_PHASE = 1;
const PENDING_LAYOUT_PHASE = 2;
const PENDING_PASSIVE_PHASE = 3;
let pendingEffectsStatus: 0 | 1 | 2 | 3 = 0;
const PENDING_AFTER_MUTATION_PHASE = 2;
const PENDING_LAYOUT_PHASE = 3;
const PENDING_PASSIVE_PHASE = 4;
let pendingEffectsStatus: 0 | 1 | 2 | 3 | 4 = 0;
let pendingEffectsRoot: FiberRoot = (null: any);
let pendingFinishedWork: Fiber = (null: any);
let pendingEffectsLanes: Lanes = NoLanes;
@@ -1269,6 +1283,7 @@ function finishConcurrentRender(
lanes,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
workInProgressAppearingViewTransitions,
workInProgressRootDidIncludeRecursiveRenderUpdate,
workInProgressDeferredLane,
workInProgressRootInterleavedUpdatedLanes,
@@ -1318,6 +1333,7 @@ function finishConcurrentRender(
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
workInProgressAppearingViewTransitions,
workInProgressRootDidIncludeRecursiveRenderUpdate,
lanes,
workInProgressDeferredLane,
@@ -1339,6 +1355,7 @@ function finishConcurrentRender(
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
workInProgressAppearingViewTransitions,
workInProgressRootDidIncludeRecursiveRenderUpdate,
lanes,
workInProgressDeferredLane,
@@ -1358,6 +1375,7 @@ function commitRootWhenReady(
finishedWork: Fiber,
recoverableErrors: Array<CapturedValue<mixed>> | null,
transitions: Array<Transition> | null,
appearingViewTransitions: Map<string, ViewTransitionInstance> | null,
didIncludeRenderPhaseUpdate: boolean,
lanes: Lanes,
spawnedLane: Lane,
@@ -1408,6 +1426,7 @@ function commitRootWhenReady(
lanes,
recoverableErrors,
transitions,
appearingViewTransitions,
didIncludeRenderPhaseUpdate,
spawnedLane,
updatedLanes,
@@ -1431,6 +1450,7 @@ function commitRootWhenReady(
lanes,
recoverableErrors,
transitions,
appearingViewTransitions,
didIncludeRenderPhaseUpdate,
spawnedLane,
updatedLanes,
@@ -1900,6 +1920,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
workInProgressRootConcurrentErrors = null;
workInProgressRootRecoverableErrors = null;
workInProgressRootDidIncludeRecursiveRenderUpdate = false;
workInProgressAppearingViewTransitions = null;
// Get the lanes that are entangled with whatever we're about to render. We
// track these separately so we can distinguish the priority of the render
@@ -2239,6 +2260,25 @@ export function renderHasNotSuspendedYet(): boolean {
return workInProgressRootExitStatus === RootInProgress;
}
export function trackAppearingViewTransition(
instance: ViewTransitionInstance,
name: string,
): void {
if (workInProgressAppearingViewTransitions === null) {
if (
!includesOnlyViewTransitionEligibleLanes(workInProgressRootRenderLanes)
) {
return;
}
workInProgressAppearingViewTransitions = new Map();
}
// Reset the pair in case we didn't end up restoring the instance in previous commits.
// This could happen since we don't actually commit all tracked instances if they end
// up in a non-committed subtree.
instance.paired = null;
workInProgressAppearingViewTransitions.set(name, instance);
}
// TODO: Over time, this function and renderRootConcurrent have become more
// and more similar. Not sure it makes sense to maintain forked paths. Consider
// unifying them again.
@@ -3148,6 +3188,7 @@ function commitRoot(
lanes: Lanes,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
appearingViewTransitions: Map<string, ViewTransitionInstance> | null,
didIncludeRenderPhaseUpdate: boolean,
spawnedLane: Lane,
updatedLanes: Lanes,
@@ -3283,13 +3324,17 @@ function commitRoot(
// might get scheduled in the commit phase. (See #16714.)
// TODO: Delete all other places that schedule the passive effect callback
// They're redundant.
const passiveSubtreeMask =
enableViewTransition && includesOnlyViewTransitionEligibleLanes(lanes)
? PassiveTransitionMask
: PassiveMask;
if (
// If this subtree rendered with profiling this commit, we need to visit it to log it.
(enableProfilerTimer &&
enableComponentPerformanceTrack &&
finishedWork.actualDuration !== 0) ||
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
(finishedWork.subtreeFlags & passiveSubtreeMask) !== NoFlags ||
(finishedWork.flags & passiveSubtreeMask) !== NoFlags
) {
if (enableYieldingBeforePassive) {
// We don't schedule a separate task for flushing passive effects.
@@ -3359,7 +3404,12 @@ function commitRoot(
// The first phase a "before mutation" phase. We use this phase to read the
// state of the host tree right before we mutate it. This is where
// getSnapshotBeforeUpdate is called.
commitBeforeMutationEffects(root, finishedWork);
commitBeforeMutationEffects(
root,
finishedWork,
lanes,
appearingViewTransitions,
);
} finally {
// Reset the priority to the previous non-sync value.
executionContext = prevExecutionContext;
@@ -3368,8 +3418,38 @@ function commitRoot(
}
}
pendingEffectsStatus = PENDING_MUTATION_PHASE;
flushMutationEffects();
flushLayoutEffects();
const startedViewTransition =
enableViewTransition &&
shouldStartViewTransition &&
startViewTransition(
root.containerInfo,
flushMutationEffects,
flushAfterMutationEffects,
flushLayoutEffects,
// TODO: This flushes passive effects at the end of the transition but
// we also schedule work to flush them separately which we really shouldn't.
// We use flushPendingEffects instead of
flushPassiveEffects,
);
if (!startedViewTransition) {
// Flush synchronously.
flushMutationEffects();
// Skip flushAfterMutationEffects
pendingEffectsStatus = PENDING_LAYOUT_PHASE;
flushLayoutEffects();
}
}
function flushAfterMutationEffects(): void {
if (pendingEffectsStatus !== PENDING_AFTER_MUTATION_PHASE) {
return;
}
pendingEffectsStatus = NO_PENDING_EFFECTS;
const root = pendingEffectsRoot;
const finishedWork = pendingFinishedWork;
const lanes = pendingEffectsLanes;
commitAfterMutationEffects(root, finishedWork, lanes);
pendingEffectsStatus = PENDING_LAYOUT_PHASE;
}
function flushMutationEffects(): void {
@@ -3415,7 +3495,7 @@ function flushMutationEffects(): void {
// componentWillUnmount, but before the layout phase, so that the finished
// work is current during componentDidMount/Update.
root.current = finishedWork;
pendingEffectsStatus = PENDING_LAYOUT_PHASE;
pendingEffectsStatus = PENDING_AFTER_MUTATION_PHASE;
}
function flushLayoutEffects(): void {
@@ -3477,12 +3557,16 @@ function flushLayoutEffects(): void {
);
}
const passiveSubtreeMask =
enableViewTransition && includesOnlyViewTransitionEligibleLanes(lanes)
? PassiveTransitionMask
: PassiveMask;
const rootDidHavePassiveEffects = // If this subtree rendered with profiling this commit, we need to visit it to log it.
(enableProfilerTimer &&
enableComponentPerformanceTrack &&
finishedWork.actualDuration !== 0) ||
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags;
(finishedWork.subtreeFlags & passiveSubtreeMask) !== NoFlags ||
(finishedWork.flags & passiveSubtreeMask) !== NoFlags;
if (rootDidHavePassiveEffects) {
pendingEffectsStatus = PENDING_PASSIVE_PHASE;
@@ -3698,6 +3782,7 @@ export function flushPendingEffects(wasDelayedCommit?: boolean): boolean {
// Returns whether passive effects were flushed.
flushMutationEffects();
flushLayoutEffects();
flushAfterMutationEffects();
return flushPassiveEffects(wasDelayedCommit);
}
+3 -1
View File
@@ -37,7 +37,8 @@ export type WorkTag =
| 26
| 27
| 28
| 29;
| 29
| 30;
export const FunctionComponent = 0;
export const ClassComponent = 1;
@@ -67,3 +68,4 @@ export const HostHoistable = 26;
export const HostSingleton = 27;
export const IncompleteFunctionComponent = 28;
export const Throw = 29;
export const ViewTransitionComponent = 30;
@@ -40,6 +40,7 @@ export opaque type NoTimeout = mixed;
export opaque type RendererInspectionConfig = mixed;
export opaque type TransitionStatus = mixed;
export opaque type FormInstance = mixed;
export opaque type InstanceMeasurement = mixed;
export type EventResponder = any;
export const rendererVersion = $$$config.rendererVersion;
@@ -128,6 +129,18 @@ export const hideInstance = $$$config.hideInstance;
export const hideTextInstance = $$$config.hideTextInstance;
export const unhideInstance = $$$config.unhideInstance;
export const unhideTextInstance = $$$config.unhideTextInstance;
export const applyViewTransitionName = $$$config.applyViewTransitionName;
export const restoreViewTransitionName = $$$config.restoreViewTransitionName;
export const cancelViewTransitionName = $$$config.cancelViewTransitionName;
export const cancelRootViewTransitionName =
$$$config.cancelRootViewTransitionName;
export const restoreRootViewTransitionName =
$$$config.restoreRootViewTransitionName;
export const measureInstance = $$$config.measureInstance;
export const wasInstanceInViewport = $$$config.wasInstanceInViewport;
export const hasInstanceChanged = $$$config.hasInstanceChanged;
export const hasInstanceAffectedParent = $$$config.hasInstanceAffectedParent;
export const startViewTransition = $$$config.startViewTransition;
export const clearContainer = $$$config.clearContainer;
// -------------------
+9 -1
View File
@@ -23,9 +23,13 @@ import {
REACT_LAZY_TYPE,
REACT_SUSPENSE_LIST_TYPE,
REACT_SUSPENSE_TYPE,
REACT_VIEW_TRANSITION_TYPE,
} from 'shared/ReactSymbols';
import {enableOwnerStacks} from 'shared/ReactFeatureFlags';
import {
enableOwnerStacks,
enableViewTransition,
} from 'shared/ReactFeatureFlags';
import {formatOwnerStack} from 'shared/ReactOwnerStackFrames';
@@ -95,6 +99,10 @@ function describeComponentStackByType(
case REACT_SUSPENSE_TYPE: {
return describeBuiltInComponentFrame('Suspense');
}
case REACT_VIEW_TRANSITION_TYPE:
if (enableViewTransition) {
return describeBuiltInComponentFrame('ViewTransition');
}
}
return '';
}
+12
View File
@@ -146,6 +146,7 @@ import {
REACT_SCOPE_TYPE,
REACT_OFFSCREEN_TYPE,
REACT_POSTPONE_TYPE,
REACT_VIEW_TRANSITION_TYPE,
} from 'shared/ReactSymbols';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
@@ -158,6 +159,7 @@ import {
disableDefaultPropsExceptForClasses,
enableAsyncIterableChildren,
enableOwnerStacks,
enableViewTransition,
} from 'shared/ReactFeatureFlags';
import assign from 'shared/assign';
@@ -2155,6 +2157,16 @@ function renderElement(
task.keyPath = prevKeyPath;
return;
}
case REACT_VIEW_TRANSITION_TYPE: {
if (enableViewTransition) {
const prevKeyPath = task.keyPath;
task.keyPath = keyPath;
renderNodeDestructive(request, task, props.children, -1);
task.keyPath = prevKeyPath;
return;
}
// Fallthrough
}
case REACT_SCOPE_TYPE: {
if (enableScopeAPI) {
const prevKeyPath = task.keyPath;
@@ -305,6 +305,72 @@ export function unhideTextInstance(
textInstance.isHidden = false;
}
export function applyViewTransitionName(
instance: Instance,
name: string,
): void {
// Noop
}
export function restoreViewTransitionName(
instance: Instance,
props: Props,
): void {
// Noop
}
export function cancelViewTransitionName(
instance: Instance,
name: string,
props: Props,
): void {
// Noop
}
export function cancelRootViewTransitionName(rootContainer: Container): void {
// Noop
}
export function restoreRootViewTransitionName(rootContainer: Container): void {
// Noop
}
export type InstanceMeasurement = null;
export function measureInstance(instance: Instance): InstanceMeasurement {
return null;
}
export function wasInstanceInViewport(
measurement: InstanceMeasurement,
): boolean {
return true;
}
export function hasInstanceChanged(
oldMeasurement: InstanceMeasurement,
newMeasurement: InstanceMeasurement,
): boolean {
return false;
}
export function hasInstanceAffectedParent(
oldMeasurement: InstanceMeasurement,
newMeasurement: InstanceMeasurement,
): boolean {
return false;
}
export function startViewTransition(
rootContainer: Container,
mutationCallback: () => void,
afterMutationCallback: () => void,
layoutCallback: () => void,
passiveCallback: () => mixed,
): boolean {
return false;
}
export function getInstanceFromNode(mockNode: Object): Object | null {
const instance = nodeToInstanceMap.get(mockNode);
if (instance !== undefined) {
@@ -32,6 +32,7 @@ export {
unstable_postpone,
unstable_getCacheForType,
unstable_SuspenseList,
unstable_ViewTransition,
unstable_useCacheRefresh,
useId,
useCallback,
+1
View File
@@ -32,6 +32,7 @@ export {
unstable_postpone,
unstable_getCacheForType,
unstable_SuspenseList,
unstable_ViewTransition,
unstable_useCacheRefresh,
useId,
useCallback,
+1
View File
@@ -51,6 +51,7 @@ export {
unstable_Scope,
unstable_SuspenseList,
unstable_TracingMarker,
unstable_ViewTransition,
unstable_getCacheForType,
unstable_useCacheRefresh,
useId,
+3
View File
@@ -18,6 +18,7 @@ import {
REACT_OFFSCREEN_TYPE,
REACT_SCOPE_TYPE,
REACT_TRACING_MARKER_TYPE,
REACT_VIEW_TRANSITION_TYPE,
} from 'shared/ReactSymbols';
import {Component, PureComponent} from './ReactBaseClasses';
@@ -123,6 +124,8 @@ export {
REACT_SCOPE_TYPE as unstable_Scope,
// enableTransitionTracing
REACT_TRACING_MARKER_TYPE as unstable_TracingMarker,
// enableViewTransition
REACT_VIEW_TRANSITION_TYPE as unstable_ViewTransition,
useId,
act, // DEV-only
captureOwnerStack, // DEV-only
@@ -15,6 +15,8 @@ import {
REACT_PROFILER_TYPE,
REACT_STRICT_MODE_TYPE,
REACT_SUSPENSE_TYPE,
REACT_SUSPENSE_LIST_TYPE,
REACT_VIEW_TRANSITION_TYPE,
} from 'shared/ReactSymbols';
import {
cloneElement,
@@ -70,7 +72,6 @@ export {
memo,
cache,
startTransition,
REACT_SUSPENSE_TYPE as unstable_SuspenseList,
getCacheForType as unstable_getCacheForType,
postpone as unstable_postpone,
useId,
@@ -79,5 +80,8 @@ export {
useMemo,
useActionState,
version,
// Experimental
REACT_SUSPENSE_LIST_TYPE as unstable_SuspenseList,
REACT_VIEW_TRANSITION_TYPE as unstable_ViewTransition,
captureOwnerStack, // DEV-only
};
@@ -15,6 +15,8 @@ import {
REACT_PROFILER_TYPE,
REACT_STRICT_MODE_TYPE,
REACT_SUSPENSE_TYPE,
REACT_SUSPENSE_LIST_TYPE,
REACT_VIEW_TRANSITION_TYPE,
} from 'shared/ReactSymbols';
import {
cloneElement,
@@ -69,7 +71,6 @@ export {
memo,
cache,
startTransition,
REACT_SUSPENSE_TYPE as unstable_SuspenseList,
getCacheForType as unstable_getCacheForType,
postpone as unstable_postpone,
useId,
@@ -78,4 +79,7 @@ export {
useMemo,
useActionState,
version,
// Experimental
REACT_SUSPENSE_LIST_TYPE as unstable_SuspenseList,
REACT_VIEW_TRANSITION_TYPE as unstable_ViewTransition,
};
@@ -15,6 +15,7 @@ import {
REACT_FORWARD_REF_TYPE,
REACT_MEMO_TYPE,
REACT_LAZY_TYPE,
REACT_VIEW_TRANSITION_TYPE,
} from 'shared/ReactSymbols';
import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
@@ -23,6 +24,8 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
import DefaultPrepareStackTrace from 'shared/DefaultPrepareStackTrace';
import {enableViewTransition} from 'shared/ReactFeatureFlags';
let prefix;
let suffix;
export function describeBuiltInComponentFrame(name: string): string {
@@ -322,6 +325,10 @@ export function describeUnknownElementTypeFrameInDEV(type: any): string {
return describeBuiltInComponentFrame('Suspense');
case REACT_SUSPENSE_LIST_TYPE:
return describeBuiltInComponentFrame('SuspenseList');
case REACT_VIEW_TRANSITION_TYPE:
if (enableViewTransition) {
return describeBuiltInComponentFrame('ViewTransition');
}
}
if (typeof type === 'object') {
switch (type.$$typeof) {
+2
View File
@@ -90,6 +90,8 @@ export const enablePostpone = __EXPERIMENTAL__;
export const enableHalt = __EXPERIMENTAL__;
export const enableViewTransition = __EXPERIMENTAL__;
/**
* Switches the Fabric API from doing layout in commit work instead of complete work.
*/
@@ -14,6 +14,7 @@ import {
REACT_MEMO_TYPE,
REACT_SUSPENSE_TYPE,
REACT_SUSPENSE_LIST_TYPE,
REACT_VIEW_TRANSITION_TYPE,
} from 'shared/ReactSymbols';
import type {LazyComponent} from 'react/src/ReactLazy';
@@ -21,6 +22,8 @@ import type {LazyComponent} from 'react/src/ReactLazy';
import isArray from 'shared/isArray';
import getPrototypeOf from 'shared/getPrototypeOf';
import {enableViewTransition} from 'shared/ReactFeatureFlags';
// Used for DEV messages to keep track of which parent rendered some props,
// in case they error.
export const jsxPropsParents: WeakMap<any, any> = new WeakMap();
@@ -129,6 +132,10 @@ function describeElementType(type: any): string {
return 'Suspense';
case REACT_SUSPENSE_LIST_TYPE:
return 'SuspenseList';
case REACT_VIEW_TRANSITION_TYPE:
if (enableViewTransition) {
return 'ViewTransition';
}
}
if (typeof type === 'object') {
switch (type.$$typeof) {
+4
View File
@@ -47,6 +47,10 @@ export const REACT_MEMO_CACHE_SENTINEL: symbol = Symbol.for(
export const REACT_POSTPONE_TYPE: symbol = Symbol.for('react.postpone');
export const REACT_VIEW_TRANSITION_TYPE: symbol = Symbol.for(
'react.view_transition',
);
const MAYBE_ITERATOR_SYMBOL = Symbol.iterator;
const FAUX_ITERATOR_SYMBOL = '@@iterator';
@@ -80,6 +80,7 @@ export const transitionLaneExpirationMs = 5000;
export const enableHydrationLaneScheduling = true;
export const enableYieldingBeforePassive = false;
export const enableThrottledScheduling = false;
export const enableViewTransition = false;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
@@ -72,6 +72,7 @@ export const enableHydrationLaneScheduling = true;
export const enableYieldingBeforePassive = false;
export const enableThrottledScheduling = false;
export const enableViewTransition = false;
// Profiling Only
export const enableProfilerTimer = __PROFILE__;
@@ -71,6 +71,7 @@ export const enableUseResourceEffectHook = false;
export const enableYieldingBeforePassive = true;
export const enableThrottledScheduling = false;
export const enableViewTransition = false;
// TODO: This must be in sync with the main ReactFeatureFlags file because
// the Test Renderer's value must be the same as the one used by the
@@ -68,6 +68,7 @@ export const enableUseResourceEffectHook = true;
export const enableHydrationLaneScheduling = true;
export const enableYieldingBeforePassive = false;
export const enableThrottledScheduling = false;
export const enableViewTransition = false;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
@@ -83,6 +83,7 @@ export const enableHydrationLaneScheduling = true;
export const enableYieldingBeforePassive = false;
export const enableThrottledScheduling = false;
export const enableViewTransition = false;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
@@ -58,6 +58,7 @@ export const enableLegacyFBSupport = true;
export const enableYieldingBeforePassive = false;
export const enableThrottledScheduling = false;
export const enableViewTransition = false;
export const enableHydrationLaneScheduling = true;
@@ -24,11 +24,13 @@ import {
REACT_SUSPENSE_LIST_TYPE,
REACT_LAZY_TYPE,
REACT_TRACING_MARKER_TYPE,
REACT_VIEW_TRANSITION_TYPE,
} from 'shared/ReactSymbols';
import {
enableTransitionTracing,
enableRenderableContext,
enableViewTransition,
} from './ReactFeatureFlags';
// Keep in sync with react-reconciler/getComponentNameFromFiber
@@ -82,6 +84,11 @@ export default function getComponentNameFromType(type: mixed): string | null {
case REACT_SUSPENSE_LIST_TYPE:
return 'SuspenseList';
// Fall through
case REACT_VIEW_TRANSITION_TYPE:
if (enableViewTransition) {
return 'ViewTransition';
}
// Fall through
case REACT_TRACING_MARKER_TYPE:
if (enableTransitionTracing) {
return 'TracingMarker';
+4 -1
View File
@@ -23,12 +23,14 @@ import {
REACT_LEGACY_HIDDEN_TYPE,
REACT_OFFSCREEN_TYPE,
REACT_TRACING_MARKER_TYPE,
REACT_VIEW_TRANSITION_TYPE,
} from 'shared/ReactSymbols';
import {
enableScopeAPI,
enableTransitionTracing,
enableLegacyHidden,
enableRenderableContext,
enableViewTransition,
} from './ReactFeatureFlags';
const REACT_CLIENT_REFERENCE: symbol = Symbol.for('react.client.reference');
@@ -50,7 +52,8 @@ export default function isValidElementType(type: mixed): boolean {
(enableLegacyHidden && type === REACT_LEGACY_HIDDEN_TYPE) ||
type === REACT_OFFSCREEN_TYPE ||
(enableScopeAPI && type === REACT_SCOPE_TYPE) ||
(enableTransitionTracing && type === REACT_TRACING_MARKER_TYPE)
(enableTransitionTracing && type === REACT_TRACING_MARKER_TYPE) ||
(enableViewTransition && type === REACT_VIEW_TRANSITION_TYPE)
) {
return true;
}
+2 -1
View File
@@ -528,5 +528,6 @@
"540": "String chunks need to be passed in their original shape. Not split into smaller string chunks. This is a bug in the wiring of the React streams.",
"541": "Compared context values must be arrays",
"542": "Suspense Exception: This is not a real error! It's an implementation detail of `useActionState` to interrupt the current render. You must either rethrow it immediately, or move the `useActionState` call outside of the `try/catch` block. Capturing without rethrowing will lead to unexpected behavior.\n\nTo handle async errors, wrap your component in an error boundary.",
"543": "Expected a ResourceEffectUpdate to be pushed together with ResourceEffectIdentity. This is a bug in React."
"543": "Expected a ResourceEffectUpdate to be pushed together with ResourceEffectIdentity. This is a bug in React.",
"544": "Found a pair with an auto name. This is a bug in React."
}