@samitouri / QOS-React-2 / commits / e0c99c4ea1

Rename <ViewTransition className="..."> to <ViewTransition default="..."> (#32734)

It was always confusing that this is not a CSS class but a view-transition-class. The `className` sticks out a bit among its siblings `enter`, `exit`, `update` and `share`. The idea is that the most specific definition override is the class name that gets applied and this prop is really just the fallback, catch-all or "any" that is applied if you didn't specify a more specific one. It has also since evolved not just to take a string but also a map of Transition Type to strings. The "class" is really the type of the value. We could add a suffix to all of them like `defaultClass`, `enterClass`, `exitClass`, `updateClass` and `shareClass`. However, this doesn't necessarily make sense with the mapping of Transition Type to string. It also makes it a bit too DOM centric. In React Native this might still be called a "class" but it might be represented by an object definition. We might even allow some kind of inline style form for the DOM too. Really this is about picking which "animation" that runs which can be a string or instance. "Animation" is too broad because there's also a concept of a CSS Animation and these are really sets of CSS animations (group, image-pair, old, new). It could maybe be `defaultTransition`, `enterTransition`, etc but that seems unnecessarily repetitive and still doesn't say anything about it being a class. We also already have the name "default" in the map of Transition Types. In fact you can now specify a default for default: ``` <ViewTransition default={{"navigation-back": "slide-out", "default": "fade-in"}}> ``` One thing I don't like about the name `"default"` is that it might be common to just apply a named class that does it matching to enter/exit/update in the CSS selectors (such as the `:only-child` rule) instead of doing that mapping to each one using React. In that can you end up specifying only `default={...}` a lot and then what is it the "default" for? It's more like "all". I think it's likely that you end up with either "default" or the specific forms instead of both at once.

Sebastian Markbåge committed Mar 26, 2025 at 15:02 UTC e0c99c4ea1cae566ad8040180cf180ae058cb8bf
5 files changed +40 -25
fixtures/view-transition/src/components/Page.js
+4 -4
@@ -33,7 +33,7 @@ const b = (
33 function Component() {
34 return (
35 <ViewTransition
36 - className={
36 + default={
37 transitions['enter-slide-right'] + ' ' + transitions['exit-slide-left']
38 }>
39 <p className="roboto-font">Slide In from Left, Slide Out to Right</p>
@@ -97,17 +97,17 @@ export default function Page({url, navigate}) {
97 }}>
98 {url === '/?b' ? 'Goto A' : 'Goto B'}
99 </button>
100 - <ViewTransition className="none">
100 + <ViewTransition default="none">
101 <div>
102 <ViewTransition>
103 <div>
104 - <ViewTransition className={transitions['slide-on-nav']}>
104 + <ViewTransition default={transitions['slide-on-nav']}>
105 <h1>{!show ? 'A' : 'B' + counter}</h1>
106 </ViewTransition>
107 </div>
108 </ViewTransition>
109 <ViewTransition
110 - className={{
110 + default={{
111 'navigation-back': transitions['slide-right'],
112 'navigation-forward': transitions['slide-left'],
113 }}>
packages/react-reconciler/src/ReactFiberApplyGesture.js
+6 -6
@@ -151,7 +151,7 @@ function trackDeletedPairViewTransitions(deletion: Fiber): void {
151 // and can stop searching (size reaches zero).
152 pairs.delete(name);
153 const className: ?string = getViewTransitionClassName(
154 - props.className,
154 + props.default,
155 props.share,
156 );
157 if (className !== 'none') {
@@ -196,7 +196,7 @@ function trackEnterViewTransitions(deletion: Fiber): void {
196 ? appearingViewTransitions.get(name)
197 : undefined;
198 const className: ?string = getViewTransitionClassName(
199 - props.className,
199 + props.default,
200 pair !== undefined ? props.share : props.enter,
201 );
202 if (className !== 'none') {
@@ -259,7 +259,7 @@ function applyAppearingPairViewTransition(child: Fiber): void {
259 // Note that this class name that doesn't actually really matter because the
260 // "new" side will be the one that wins in practice.
261 const className: ?string = getViewTransitionClassName(
262 - props.className,
262 + props.default,
263 props.share,
264 );
265 if (className !== 'none') {
@@ -282,7 +282,7 @@ function applyExitViewTransition(placement: Fiber): void {
282 const props: ViewTransitionProps = placement.memoizedProps;
283 const name = getViewTransitionName(props, state);
284 const className: ?string = getViewTransitionClassName(
285 - props.className,
285 + props.default,
286 // Note that just because we don't have a pair yet doesn't mean we won't find one
287 // later. However, that doesn't matter because if we do the class name that wins
288 // is the one applied by the "new" side anyway.
@@ -307,7 +307,7 @@ function applyNestedViewTransition(child: Fiber): void {
307 const props: ViewTransitionProps = child.memoizedProps;
308 const name = getViewTransitionName(props, state);
309 const className: ?string = getViewTransitionClassName(
310 - props.className,
310 + props.default,
311 props.update,
312 );
313 if (className !== 'none') {
@@ -336,7 +336,7 @@ function applyUpdateViewTransition(current: Fiber, finishedWork: Fiber): void {
336 // want the props from "current" since that's the class that would've won if
337 // it was the normal direction. To preserve the same effect in either direction.
338 const className: ?string = getViewTransitionClassName(
339 - newProps.className,
339 + newProps.default,
340 newProps.update,
341 );
342 if (className === 'none') {
packages/react-reconciler/src/ReactFiberBeginWork.js
+21
@@ -322,6 +322,7 @@ export let didWarnAboutReassigningProps: boolean;
322 let didWarnAboutRevealOrder;
323 let didWarnAboutTailOptions;
324 let didWarnAboutDefaultPropsOnFunctionComponent;
325 +let didWarnAboutClassNameOnViewTransition;
326
327 if (__DEV__) {
328 didWarnAboutBadClass = ({}: {[string]: boolean});
@@ -332,6 +333,7 @@ if (__DEV__) {
333 didWarnAboutRevealOrder = ({}: {[empty]: boolean});
334 didWarnAboutTailOptions = ({}: {[string]: boolean});
335 didWarnAboutDefaultPropsOnFunctionComponent = ({}: {[string]: boolean});
336 + didWarnAboutClassNameOnViewTransition = ({}: {[string]: boolean});
337 }
338
339 export function reconcileChildren(
@@ -3295,6 +3297,25 @@ function updateViewTransition(
3297 pushMaterializedTreeId(workInProgress);
3298 }
3299 }
3300 + if (__DEV__) {
3301 + // $FlowFixMe[prop-missing]
3302 + if (pendingProps.className !== undefined) {
3303 + const example =
3304 + typeof pendingProps.className === 'string'
3305 + ? JSON.stringify(pendingProps.className)
3306 + : '{...}';
3307 + if (!didWarnAboutClassNameOnViewTransition[example]) {
3308 + didWarnAboutClassNameOnViewTransition[example] = true;
3309 + console.error(
3310 + '<ViewTransition> doesn\'t accept a "className" prop. It has been renamed to "default".\n' +
3311 + '- <ViewTransition className=%s>\n' +
3312 + '+ <ViewTransition default=%s>',
3313 + example,
3314 + example,
3315 + );
3316 + }
3317 + }
3318 + }
3319 if (current !== null && current.memoizedProps.name !== pendingProps.name) {
3320 // If the name changes, we schedule a ref effect to create a new ref instance.
3321 workInProgress.flags |= Ref | RefStatic;
packages/react-reconciler/src/ReactFiberCommitViewTransitions.js
+8 -8
@@ -228,7 +228,7 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
228 }
229 const name = props.name;
230 const className: ?string = getViewTransitionClassName(
231 - props.className,
231 + props.default,
232 props.share,
233 );
234 if (className !== 'none') {
@@ -267,7 +267,7 @@ export function commitEnterViewTransitions(
267 const props: ViewTransitionProps = placement.memoizedProps;
268 const name = getViewTransitionName(props, state);
269 const className: ?string = getViewTransitionClassName(
270 - props.className,
270 + props.default,
271 state.paired ? props.share : props.enter,
272 );
273 if (className !== 'none') {
@@ -337,7 +337,7 @@ function commitDeletedPairViewTransitions(deletion: Fiber): void {
337 const pair = pairs.get(name);
338 if (pair !== undefined) {
339 const className: ?string = getViewTransitionClassName(
340 - props.className,
340 + props.default,
341 props.share,
342 );
343 if (className !== 'none') {
@@ -389,7 +389,7 @@ export function commitExitViewTransitions(deletion: Fiber): void {
389 ? appearingViewTransitions.get(name)
390 : undefined;
391 const className: ?string = getViewTransitionClassName(
392 - props.className,
392 + props.default,
393 pair !== undefined ? props.share : props.exit,
394 );
395 if (className !== 'none') {
@@ -470,7 +470,7 @@ export function commitBeforeUpdateViewTransition(
470 // a layout only change, then the "foo" class will be applied even though
471 // it was not actually an update. Which is a bug.
472 const className: ?string = getViewTransitionClassName(
473 - newProps.className,
473 + newProps.default,
474 newProps.update,
475 );
476 if (className === 'none') {
@@ -495,7 +495,7 @@ export function commitNestedViewTransitions(changedParent: Fiber): void {
495 const props: ViewTransitionProps = child.memoizedProps;
496 const name = getViewTransitionName(props, child.stateNode);
497 const className: ?string = getViewTransitionClassName(
498 - props.className,
498 + props.default,
499 props.update,
500 );
501 if (className !== 'none') {
@@ -735,7 +735,7 @@ export function measureUpdateViewTransition(
735 const oldName = getViewTransitionName(oldFiber.memoizedProps, state);
736 // Whether it ends up having been updated or relayout we apply the update class name.
737 const className: ?string = getViewTransitionClassName(
738 - props.className,
738 + props.default,
739 props.update,
740 );
741 if (className === 'none') {
@@ -787,7 +787,7 @@ export function measureNestedViewTransitions(
787 const state: ViewTransitionState = child.stateNode;
788 const name = getViewTransitionName(props, state);
789 const className: ?string = getViewTransitionClassName(
790 - props.className,
790 + props.default,
791 props.update,
792 );
793 let previousMeasurements: null | Array<InstanceMeasurement>;
packages/react-reconciler/src/ReactFiberViewTransitionComponent.js
+1 -7
@@ -29,7 +29,7 @@ export type ViewTransitionClass = 'none' | string | ViewTransitionClassPerType;
29 export type ViewTransitionProps = {
30 name?: string,
31 children?: ReactNodeList,
32 - className?: ViewTransitionClass,
32 + default?: ViewTransitionClass,
33 enter?: ViewTransitionClass,
34 exit?: ViewTransitionClass,
35 share?: ViewTransitionClass,
@@ -129,11 +129,5 @@ export function getViewTransitionClassName(
129 if (eventClassName == null) {
130 return className;
131 }
132 - if (eventClassName === 'none') {
133 - return eventClassName;
134 - }
135 - if (className != null && className !== 'none') {
136 - return className + ' ' + eventClassName;
137 - }
132 return eventClassName;
133 }