Skip to content

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

Merged
merged 33 commits into from
Jul 1, 2022
Merged

Feat: Add deferred support #9002

merged 33 commits into from
Jul 1, 2022

Conversation

brophdawg11
Copy link
Contributor

  • Add's deferred() support to loader's in data routers
  • Unit tests cover main usages
  • Can be manually tested via the examples/data-router app using the /deferred and /deferred/child routes
    • probably need to run via USE_SOURCE=true npm run dev

@changeset-bot
Copy link

changeset-bot bot commented Jun 21, 2022

🦋 Changeset detected

Latest commit: 57e1fe7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
react-router Patch
@remix-run/router Patch
react-router-dom Patch
react-router-dom-v5-compat Patch
react-router-native Patch

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 {
Copy link
Contributor Author

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;
Copy link
Contributor Author

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"
Copy link
Contributor Author

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;
Copy link
Member

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 7504d99

@ryanflorence
Copy link
Member

Can you access the deferred error somewhere?

@brophdawg11
Copy link
Contributor Author

Can you access the deferred error somewhere?

I didn't create a specific hook for it yet - both success and error come through useDeferred and we provide the isDeferredError utility to differentiate. We can add one if we want the clarity. There's a few ways to consume the error currently:

// 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 useDeferred into useDeferredData/useDeferredError it would look like:

// 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. useDeferredData/useDeferredError aligns more closely to useLoaderData/useRouteError too. I can go ahead and make that change.

@ryanflorence
Copy link
Member

ryanflorence commented Jun 22, 2022

I imagined we'd be able to use the same ErrorBoundary for route errorElement or deferred errorElement.

<>
  <Route errorElement={<ErrorBoundary />} />
  <Deferred errorElement={<ErrorBoundary />} />
</>

function ErrorBoundary() {
  let error = useRouteError();
  // ...
}

wdyt?

@brophdawg11
Copy link
Contributor Author

Hm - yeah that could work - so the children of Deferred behave like a <Route element> and are always happy path?

@ryanflorence
Copy link
Member

ryanflorence commented Jun 22, 2022

are always happy path?

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.

<>
<p>Critical Data: {data.critical}</p>
<Deferred
data={data.lazy}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value={data.lazy}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌

}

function RenderDeferredError() {
let data = useDeferred();
Copy link
Member

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?

Copy link
Contributor Author

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.

<>
<p>Critical Data: {data.critical}</p>
<Deferred value={data.lazy} fallback={<p>Loading...</p>}>
{({ data }) =>
Copy link
Member

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>

Copy link
Contributor Author

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

@ryanflorence ryanflorence dismissed their stale review July 1, 2022 14:41

I don't know how to use github reviews and this is annoying.

@ryanflorence ryanflorence merged commit d5b2560 into dev Jul 1, 2022
@ryanflorence ryanflorence deleted the brophdawg11/deferred branch July 1, 2022 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants