-
Notifications
You must be signed in to change notification settings - Fork 111
feat(react): utilize synchronously inspectable promise to optimize suspense fallback rendering #658
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
177aabe
Compliance test suit pass
9b4c81d
remove adapters
a604301
ealry returns
a898ee0
fix missing construction logic
2292315
wrap dataloaders with sync inspectable promises
c35cafb
optimize
17a3107
replace SyncInspectablePromise with inspectPromise
11c93be
use SyncInspectablePromise
268c9e2
Sync
41f3f7a
data loader
3e81b21
test
0b542b1
try 1
b670f55
before refactor
da28b6e
rename
924bc28
remove assertions
bbcb78b
fix type
f90f245
FIX IMPORT
f229eb8
refactor
cf96c1e
remove
e2f6060
fix import
a3a0da0
remove export
157f45f
cleanup
3f05594
fix import
2fa6fb8
add css
5a258fb
lazy
537a139
cs
43a17c5
optimize lazy
f276251
strict guarantee
6b4e0a0
refactor
6701c53
use well made polyfill
7847cf0
opt
8d21d3d
opt
122ea57
remove react18-use
b46cb23
remove unused imports
0508ac2
rename to resolve
b7360c6
rename to reject
eeecb64
fix
7a93506
fix
82ce76b
fix demo
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@stackflow/react": minor | ||
| --- | ||
|
|
||
| utilize synchronously inspectable promise to optimize suspense fallback rendering |
2 changes: 1 addition & 1 deletion
2
integrations/react/src/__internal__/LazyActivityComponentType.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import type { StaticActivityComponentType } from "./StaticActivityComponentType"; | ||
|
|
||
| export type LazyActivityComponentType<T extends {} = {}> = | ||
| React.LazyExoticComponent<StaticActivityComponentType<T>> & { | ||
| StaticActivityComponentType<T> & { | ||
| _load?: () => Promise<{ default: StaticActivityComponentType<T> }>; | ||
| }; | ||
ENvironmentSet marked this conversation as resolved.
Show resolved
Hide resolved
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
integrations/react/src/__internal__/utils/PreloadableLazyComponent.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import type { ComponentType } from "react"; | ||
| import { | ||
| inspect, | ||
| PromiseStatus, | ||
| type SyncInspectablePromise, | ||
| } from "./SyncInspectablePromise"; | ||
| import { useThenable } from "./useThenable"; | ||
|
|
||
| export function preloadableLazyComponent<P extends {}>( | ||
| load: () => SyncInspectablePromise<{ default: ComponentType<P> }>, | ||
| ): { Component: ComponentType<P>; preload: () => Promise<void> } { | ||
| let cachedLoadingPromise: SyncInspectablePromise<{ | ||
| default: ComponentType<P>; | ||
| }> | null = null; | ||
| const cachedLoad = () => { | ||
| if ( | ||
| !cachedLoadingPromise || | ||
| inspect(cachedLoadingPromise).status === PromiseStatus.REJECTED | ||
| ) { | ||
| cachedLoadingPromise = load(); | ||
| } | ||
|
|
||
| return cachedLoadingPromise; | ||
| }; | ||
| const Component = (props: P) => { | ||
| const { default: Component } = useThenable(cachedLoad()); | ||
|
|
||
| return <Component {...props} />; | ||
| }; | ||
|
|
||
| return { | ||
| Component, | ||
| preload: async () => void (await cachedLoad()), | ||
| }; | ||
| } |
97 changes: 97 additions & 0 deletions
97
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| import { isPromiseLike } from "./isPromiseLike"; | ||
|
|
||
| export interface SyncInspectablePromise<T> extends Promise<T> { | ||
| status: PromiseStatus; | ||
| value?: T; | ||
| reason?: unknown; | ||
| } | ||
|
|
||
| export const PromiseStatus = { | ||
| PENDING: "pending", | ||
| FULFILLED: "fulfilled", | ||
| REJECTED: "rejected", | ||
| } as const; | ||
| export type PromiseStatus = (typeof PromiseStatus)[keyof typeof PromiseStatus]; | ||
|
|
||
| export type PromiseState<T> = | ||
| | { | ||
| status: typeof PromiseStatus.PENDING; | ||
| } | ||
| | { | ||
| status: typeof PromiseStatus.FULFILLED; | ||
| value: T; | ||
| } | ||
| | { | ||
| status: typeof PromiseStatus.REJECTED; | ||
| reason: unknown; | ||
| }; | ||
|
|
||
| export function inspect<T>( | ||
| promise: SyncInspectablePromise<T>, | ||
| ): PromiseState<T> { | ||
| if (promise.status === PromiseStatus.PENDING) { | ||
| return { | ||
| status: PromiseStatus.PENDING, | ||
| }; | ||
| } else if (promise.status === PromiseStatus.FULFILLED && "value" in promise) { | ||
| return { | ||
| status: PromiseStatus.FULFILLED, | ||
| value: promise.value as T, | ||
| }; | ||
| } else if (promise.status === PromiseStatus.REJECTED && "reason" in promise) { | ||
| return { | ||
| status: PromiseStatus.REJECTED, | ||
| reason: promise.reason, | ||
| }; | ||
| } else { | ||
| throw new Error("Invalid promise state"); | ||
| } | ||
| } | ||
|
|
||
| function makeSyncInspectable<T>( | ||
| thenable: PromiseLike<T>, | ||
| ): SyncInspectablePromise<T> { | ||
| const syncInspectablePromise: SyncInspectablePromise<T> = Object.assign( | ||
| new Promise<T>((resolve) => resolve(thenable)), | ||
| { status: PromiseStatus.PENDING }, | ||
| ); | ||
|
|
||
| syncInspectablePromise.then( | ||
| (value) => { | ||
| syncInspectablePromise.status = PromiseStatus.FULFILLED; | ||
| syncInspectablePromise.value = value; | ||
| }, | ||
| (reason) => { | ||
| syncInspectablePromise.status = PromiseStatus.REJECTED; | ||
| syncInspectablePromise.reason = reason; | ||
| }, | ||
| ); | ||
|
|
||
| return syncInspectablePromise; | ||
| } | ||
|
|
||
| export function resolve<T>(value: T): SyncInspectablePromise<Awaited<T>> { | ||
| if (isPromiseLike(value)) { | ||
| if ( | ||
| value instanceof Promise && | ||
| "status" in value && | ||
| Object.values(PromiseStatus).some((status) => status === value.status) | ||
| ) { | ||
| return value as SyncInspectablePromise<Awaited<T>>; | ||
| } | ||
|
|
||
| return makeSyncInspectable(value) as SyncInspectablePromise<Awaited<T>>; | ||
| } | ||
|
|
||
| return Object.assign(Promise.resolve(value), { | ||
| status: PromiseStatus.FULFILLED, | ||
| value, | ||
| }) as SyncInspectablePromise<Awaited<T>>; | ||
| } | ||
|
|
||
| export function reject(error: unknown): SyncInspectablePromise<never> { | ||
| return Object.assign(Promise.reject(error), { | ||
| status: PromiseStatus.REJECTED, | ||
| reason: error, | ||
| }) as SyncInspectablePromise<never>; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { inspect, PromiseStatus, resolve } from "./SyncInspectablePromise"; | ||
|
|
||
| export function useThenable<T>(thenable: PromiseLike<T>): Awaited<T> { | ||
| const syncInspectable = resolve(thenable); | ||
| const state = inspect(syncInspectable); | ||
|
|
||
| if (state.status === PromiseStatus.FULFILLED) { | ||
| return state.value; | ||
| } else if (state.status === PromiseStatus.REJECTED) { | ||
| throw state.reason; | ||
| } | ||
|
|
||
| throw syncInspectable; // Trigger suspense by throwing the promise. | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,28 +1,37 @@ | ||
| import React from "react"; | ||
| import type { LazyActivityComponentType } from "../__internal__/LazyActivityComponentType"; | ||
| import type { StaticActivityComponentType } from "../__internal__/StaticActivityComponentType"; | ||
| import { preloadableLazyComponent } from "../__internal__/utils/PreloadableLazyComponent"; | ||
| import { | ||
| inspect, | ||
| PromiseStatus, | ||
| reject, | ||
| resolve, | ||
| } from "../__internal__/utils/SyncInspectablePromise"; | ||
|
|
||
| export function lazy<T extends { [K in keyof T]: any } = {}>( | ||
| load: () => Promise<{ default: StaticActivityComponentType<T> }>, | ||
| ): LazyActivityComponentType<T> { | ||
| let cachedValue: Promise<{ default: StaticActivityComponentType<T> }> | null = | ||
| null; | ||
| const { Component, preload } = preloadableLazyComponent(() => | ||
| resolve(load()), | ||
| ); | ||
|
|
||
| const cachedLoad = () => { | ||
| if (!cachedValue) { | ||
| cachedValue = load(); | ||
| cachedValue.catch((error) => { | ||
| cachedValue = null; | ||
| const LazyActivityComponent: LazyActivityComponentType<T> = Object.assign( | ||
| Component, | ||
| { | ||
| _load: () => { | ||
| const preloadTask = resolve(preload()); | ||
| const preloadTaskState = inspect(preloadTask); | ||
|
|
||
| throw error; | ||
| }); | ||
| } | ||
| return cachedValue; | ||
| }; | ||
| if (preloadTaskState.status === PromiseStatus.FULFILLED) { | ||
| return resolve({ default: Component }); | ||
| } else if (preloadTaskState.status === PromiseStatus.REJECTED) { | ||
| return reject(preloadTaskState.reason); | ||
| } | ||
|
|
||
| const LazyActivityComponent: LazyActivityComponentType<T> = | ||
| React.lazy(cachedLoad); | ||
| LazyActivityComponent._load = cachedLoad; | ||
| return resolve(preloadTask.then(() => ({ default: Component }))); | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| return LazyActivityComponent; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.