Skip to content
6 changes: 6 additions & 0 deletions fixtures/view-transition/src/components/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ function Component() {
transitions['enter-slide-right'] + ' ' + transitions['exit-slide-left']
}>
<p className="roboto-font">Slide In from Left, Slide Out to Right</p>
<p>
<img
src="https://react.dev/_next/image?url=%2Fimages%2Fteam%2Fsebmarkbage.jpg&w=3840&q=75"
width="300"
/>
</p>
</ViewTransition>
);
}
Expand Down
10 changes: 9 additions & 1 deletion packages/react-art/src/ReactFiberConfigART.js
Original file line number Diff line number Diff line change
Expand Up @@ -596,14 +596,22 @@ export function maySuspendCommit(type, props) {
return false;
}

export function maySuspendCommitOnUpdate(type, oldProps, newProps) {
return false;
}

export function maySuspendCommitInSyncRender(type, props) {
return false;
}

export function preloadInstance(type, props) {
// Return true to indicate it's already loaded
return true;
}

export function startSuspendingCommit() {}

export function suspendInstance(type, props) {}
export function suspendInstance(instance, type, props) {}

export function suspendOnActiveViewTransition(container) {}

Expand Down
86 changes: 80 additions & 6 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ import {
disableLegacyMode,
enableMoveBefore,
disableCommentsAsDOMContainers,
enableSuspenseyImages,
} from 'shared/ReactFeatureFlags';
import {
HostComponent,
Expand Down Expand Up @@ -145,6 +146,10 @@ export type Props = {
is?: string,
size?: number,
multiple?: boolean,
src?: string,
srcSet?: string,
loading?: 'eager' | 'lazy',
onLoad?: (event: any) => void,
...
};
type RawProps = {
Expand Down Expand Up @@ -769,9 +774,9 @@ export function commitMount(
// only need to assign one. And Safari just never triggers a new load event which means this technique
// is already a noop regardless of which properties are assigned. We should revisit if browsers update
// this heuristic in the future.
if ((newProps: any).src) {
if (newProps.src) {
((domElement: any): HTMLImageElement).src = (newProps: any).src;
} else if ((newProps: any).srcSet) {
} else if (newProps.srcSet) {
((domElement: any): HTMLImageElement).srcset = (newProps: any).srcSet;
}
return;
Expand Down Expand Up @@ -4974,6 +4979,36 @@ export function isHostHoistableType(
}

export function maySuspendCommit(type: Type, props: Props): boolean {
if (!enableSuspenseyImages) {
return false;
}
// Suspensey images are the default, unless you opt-out of with either
// loading="lazy" or onLoad={...} which implies you're ok waiting.
return (
type === 'img' &&
props.src != null &&
props.src !== '' &&
props.onLoad == null &&
props.loading !== 'lazy'
);
}

export function maySuspendCommitOnUpdate(
type: Type,
oldProps: Props,
newProps: Props,
): boolean {
return (
maySuspendCommit(type, newProps) &&
(newProps.src !== oldProps.src || newProps.srcSet !== oldProps.srcSet)
);
}

export function maySuspendCommitInSyncRender(
type: Type,
props: Props,
): boolean {
// TODO: Allow sync lanes to suspend too with an opt-in.
return false;
}

Expand All @@ -4984,8 +5019,17 @@ export function mayResourceSuspendCommit(resource: Resource): boolean {
);
}

export function preloadInstance(type: Type, props: Props): boolean {
return true;
export function preloadInstance(
instance: Instance,
type: Type,
props: Props,
): boolean {
// We don't need to preload Suspensey images because the browser will
// load them early once we set the src.
// If we return true here, we'll still get a suspendInstance call in the
// pre-commit phase to determine if we still need to decode the image or
// if was dropped from cache. This just avoids rendering Suspense fallback.
return !!(instance: any).complete;
}

export function preloadResource(resource: Resource): boolean {
Expand Down Expand Up @@ -5022,8 +5066,38 @@ export function startSuspendingCommit(): void {
};
}

export function suspendInstance(type: Type, props: Props): void {
return;
const SUSPENSEY_IMAGE_TIMEOUT = 500;

export function suspendInstance(
instance: Instance,
type: Type,
props: Props,
): void {
if (!enableSuspenseyImages) {
return;
}
if (suspendedState === null) {
throw new Error(
'Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.',
);
}
const state = suspendedState;
if (
// $FlowFixMe[prop-missing]
typeof instance.decode === 'function' &&
typeof setTimeout === 'function'
) {
// If this browser supports decode() API, we use it to suspend waiting on the image.
// The loading should have already started at this point, so it should be enough to
// just call decode() which should also wait for the data to finish loading.
state.count++;
const ping = onUnsuspend.bind(state);
Promise.race([
// $FlowFixMe[prop-missing]
instance.decode(),
new Promise(resolve => setTimeout(resolve, SUSPENSEY_IMAGE_TIMEOUT)),
]).then(ping, ping);
}
}

export function suspendResource(
Expand Down
27 changes: 25 additions & 2 deletions packages/react-native-renderer/src/ReactFiberConfigFabric.js
Original file line number Diff line number Diff line change
Expand Up @@ -577,13 +577,36 @@ export function maySuspendCommit(type: Type, props: Props): boolean {
return false;
}

export function preloadInstance(type: Type, props: Props): boolean {
export function maySuspendCommitOnUpdate(
type: Type,
oldProps: Props,
newProps: Props,
): boolean {
return false;
}

export function maySuspendCommitInSyncRender(
type: Type,
props: Props,
): boolean {
return false;
}

export function preloadInstance(
instance: Instance,
type: Type,
props: Props,
): boolean {
return true;
}

export function startSuspendingCommit(): void {}

export function suspendInstance(type: Type, props: Props): void {}
export function suspendInstance(
instance: Instance,
type: Type,
props: Props,
): void {}

export function suspendOnActiveViewTransition(container: Container): void {}

Expand Down
27 changes: 25 additions & 2 deletions packages/react-native-renderer/src/ReactFiberConfigNative.js
Original file line number Diff line number Diff line change
Expand Up @@ -735,14 +735,37 @@ export function maySuspendCommit(type: Type, props: Props): boolean {
return false;
}

export function preloadInstance(type: Type, props: Props): boolean {
export function maySuspendCommitOnUpdate(
type: Type,
oldProps: Props,
newProps: Props,
): boolean {
return false;
}

export function maySuspendCommitInSyncRender(
type: Type,
props: Props,
): boolean {
return false;
}

export function preloadInstance(
instance: Instance,
type: Type,
props: Props,
): boolean {
// Return false to indicate it's already loaded
return true;
}

export function startSuspendingCommit(): void {}

export function suspendInstance(type: Type, props: Props): void {}
export function suspendInstance(
instance: Instance,
type: Type,
props: Props,
): void {}

export function suspendOnActiveViewTransition(container: Container): void {}

Expand Down
28 changes: 26 additions & 2 deletions packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
suspenseyCommitSubscription = null;
}

function suspendInstance(type: string, props: Props): void {
function suspendInstance(
instance: Instance,
type: string,
props: Props,
): void {
const src = props.src;
if (type === 'suspensey-thing' && typeof src === 'string') {
// Attach a listener to the suspensey thing and create a subscription
Expand Down Expand Up @@ -624,13 +628,33 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
return type === 'suspensey-thing' && typeof props.src === 'string';
},

maySuspendCommitOnUpdate(
type: string,
oldProps: Props,
newProps: Props,
): boolean {
// Asks whether it's possible for this combination of type and props
// to ever need to suspend. This is different from asking whether it's
// currently ready because even if it's ready now, it might get purged
// from the cache later.
return (
type === 'suspensey-thing' &&
typeof newProps.src === 'string' &&
newProps.src !== oldProps.src
);
},

maySuspendCommitInSyncRender(type: string, props: Props): boolean {
return true;
},

mayResourceSuspendCommit(resource: mixed): boolean {
throw new Error(
'Resources are not implemented for React Noop yet. This method should not be called',
);
},

preloadInstance(type: string, props: Props): boolean {
preloadInstance(instance: Instance, type: string, props: Props): boolean {
if (type !== 'suspensey-thing' || typeof props.src !== 'string') {
throw new Error('Attempted to preload unexpected instance: ' + type);
}
Expand Down
Loading
Loading