-
-
Notifications
You must be signed in to change notification settings - Fork 10.7k
Feat: Add deferred support #9002
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
Conversation
🦋 Changeset detectedLatest commit: 57e1fe7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@@ -783,6 +841,68 @@ export const json: JsonFunction = (data, init = {}) => { | |||
}); | |||
}; | |||
|
|||
export class DeferredData { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class gets instantiated by a deferred()
call and handles tracking data and pending keys, and calling a registered subscriber function when individual promises resolve so the router can update state.
* promise rejection | ||
*/ | ||
export function isDeferredError(e: any): e is DeferredError { | ||
return e instanceof DeferredError; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is so we know if the data on loaderData
was from a promise rejection and we should render it in the <Deferred>
errorBoundary
component
updateState({ revalidation: "loading" }); | ||
|
||
// If we're currently submitting an action, we don't need to start a new | ||
// navigation, we'll just let the follow up loader execution call all loaders | ||
if ( | ||
state.navigation.state === "submitting" && | ||
state.navigation.formMethod !== "get" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was no longer necessary since all gets are now state:loading
export interface DeferredProps extends Omit<React.SuspenseProps, "children"> { | ||
children: React.ReactNode | DeferredResolveRenderFunction; | ||
data: any; | ||
errorBoundary?: React.ReactNode; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's use errorElement
so we don't have both errorElement
on Route and errorBoundary
on Deferred
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 7504d99
Can you access the deferred error somewhere? |
I didn't create a specific hook for it yet - both success and error come through // 1 - Consume success/error states in different components
<Deferred data="data" errorElement={<MyErrorBoundary/>}>
<MyData />
</Deferred>
function MyData() {
let data = useDeferred(); // Guaranteed to be success data
return <p>{data}<p>;
}
function MyErrorBoundary() {
let error = useDeferred(); // Guaranteed to be a DeferredError
return <p>{error.message}<p>;
}
//////////
// 2 - Consume success/error states in the same component
<Deferred data="data">
<MyData />
</Deferred>
function MyData() {
// This might be success or error - you need to handle
let dataOrError = useDeferred();
if (isDeferredError(dataOrError)) {
return <p>{dataOrError.message}<p>;
}
return <p>{dataOrError}<p>;
}
//////////
// 3 - Consume via render props
<Deferred data="data">
{({ data }) => isDeferredError(data) ?
<p>{data.message}<p> :
<p>{data}<p>}
</Deferred> If we want to fork // 1 - Consume success/error states in different components
<Deferred data="data" errorElement={<MyErrorBoundary/>}>
<MyData />
</Deferred>
function MyData() {
let data = useDeferredData();
return <p>{data}<p>;
}
function MyErrorBoundary() {
let error = useDeferredError();
return <p>{error.message}<p>;
}
//////////
// 2 - Consume success/error states in the same component
<Deferred data="data">
<MyData />
</Deferred>
function MyData() {
let data = useDeferredData();
let error = useDeferredError();
if (error) {
return <p>{error.message}<p>;
}
return <p>{data}<p>;
}
//////////
// 3 - Consume via render props
<Deferred data="data">
{({ data, error }) => error ?
<p>{error.message}<p> :
<p>{data}<p>}
</Deferred> It's really no more verbose in either case - so I think I agree an explicit hooks would be nice. |
I imagined we'd be able to use the same ErrorBoundary for route <>
<Route errorElement={<ErrorBoundary />} />
<Deferred errorElement={<ErrorBoundary />} />
</>
function ErrorBoundary() {
let error = useRouteError();
// ...
} wdyt? |
Hm - yeah that could work - so the |
Yeah, that's the goal. Keep happy paths happy! Also, if a deferred value throws an error and does not have an errorElement it needs to bubble up to the nearest route anyway. |
.changeset/light-months-argue.md
Outdated
<> | ||
<p>Critical Data: {data.critical}</p> | ||
<Deferred | ||
data={data.lazy} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
value={data.lazy}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙌
.changeset/light-months-argue.md
Outdated
} | ||
|
||
function RenderDeferredError() { | ||
let data = useDeferred(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we call this useDeferredValue
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stale changeset again :/. It's useDeferredData
now - aligning with useLoaderData
as its happy path. We didn't want to use useDeferredValue
since <Deferred value="...">
accepts a Promise
/DeferredError
or the resolved "data". useDeferredData
will only ever give you the resolved data.
.changeset/light-months-argue.md
Outdated
<> | ||
<p>Critical Data: {data.critical}</p> | ||
<Deferred value={data.lazy} fallback={<p>Loading...</p>}> | ||
{({ data }) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's nothing else for us to pass in here, so let's not put it on a named key and then force them to rename or double-destructure it:
// 😕
<Deferred>
{({ data: whatever }) => ()}
</Deferred>
// 😕
<Deferred>
{({ data: { stuff } }) => ()}
</Deferred>
Instead just pass the value into the child function:
<Deferred>
{value => ()}
</Deferred>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops - that's a stale changeset 😬 We do just pass the resolved data now, will get that updated
I don't know how to use github reviews and this is annoying.
deferred()
support toloader
's in data routersexamples/data-router
app using the/deferred
and/deferred/child
routesUSE_SOURCE=true npm run dev