Skip to content
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

[PoC] Parent data in child loader #11319

Closed
wants to merge 5 commits into from

Conversation

steinybot
Copy link

Fixes #9188

Here is a proof of concept for using parent data in a child loader. It still needs a bit of work but it at least proves that it can be done.

Other than the places where the parent data hasn't been hooked up, the main problem to solve (if it is even an problem) is that the child will wait for all of the deferred parent data to be resolved before the child loader runs.

Take a look at the data-router example.

Copy link

changeset-bot bot commented Mar 2, 2024

⚠️ No Changeset found

Latest commit: e710394

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@remix-cla-bot
Copy link
Contributor

remix-cla-bot bot commented Mar 2, 2024

Hi @steinybot,

Welcome, and thank you for contributing to React Router!

Before we consider your pull request, we ask that you sign our Contributor License Agreement (CLA). We require this only once.

You may review the CLA and sign it by adding your name to contributors.yml.

Once the CLA is signed, the CLA Signed label will be added to the pull request.

If you have already signed the CLA and received this response in error, or if you have any questions, please contact us at hello@remix.run.

Thanks!

- The Remix team

descendents: MatchNode[]
}

function createMatcherForest(matches: AgnosticDataRouteMatch[]): MatchNode[] {
Copy link
Author

Choose a reason for hiding this comment

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

This is possibly overkill but I didn't want to assume that the matches were in any particular order or that matches were strictly limited to a single path (in graph theory terms) even though that probably is the case (although the found router allows for multiple paths which is actually quite nice in some cases so I'd leave that capability in here since there is pretty much zero cost to it).

@timdorr
Copy link
Member

timdorr commented Mar 5, 2024

Unfortunately, this breaks one of the fundamental design decisions of the library. We don't want request waterfalls, which this specifically creates (whether the user wants it or not).

This is going to make things like RSC harder to integrate later on as well. Another reason for loader independence is that you can selectively reload parts of the tree without having to trigger reloads of ancestor components. Those ancestor reloads would then lead to descendant reloads, which means pretty much a full-page reload. That would make RSC a moot point.

I'm not going to close this in case @brophdawg11 disagrees, but this doesn't seem likely to be considered. The Middleware proposal is the more likely direction here.

@brophdawg11
Copy link
Contributor

I agree middleware is the eventual solution to this type of sequential access.

However, we are close to landing a new unstable_dataStrategy configuration in #11098 that will give users a way to opt-into advanced control over the execution of loaders and will enable implementation of middleware as well as this type of API in user-land 😄.

Thank you for the investigation! I'll try to remember to add a unit test into the "use cases" section of the dataStrategy PR demonstrating this type of API usage.

@brophdawg11 brophdawg11 closed this Mar 5, 2024
@steinybot
Copy link
Author

Unfortunately, this breaks one of the fundamental design decisions of the library. We don't want request waterfalls, which this specifically creates (whether the user wants it or not).

In my use case I am using relay with https://github.com/loop-payments/react-router-relay. In that case the loader for the parent has already fetched the data that the child needs so there is no extra network requests. All the dependent loader is doing is extracting the fragment ref from the parents data.

... which this specifically creates (whether the user wants it or not).

What do you mean? Nothing here forces a waterfall. With this you only use it when you specifically want your child loader to run after the parents. It is not intended to be used to chain network requests, although if you really want to then there is nothing stopping you. In saying that, if the parent loaded data that was immediately available (just like the example) then the child could depend on that and make a network request and there would not be a waterfall.

Another reason for loader independence is that you can selectively reload parts of the tree without having to trigger reloads of ancestor components. Those ancestor reloads would then lead to descendant reloads, which means pretty much a full-page reload.

The ancestor loaders have already resolved, you would not reload them.

I agree middleware is the eventual solution to this type of sequential access.

However, we are close to landing a new unstable_dataStrategy configuration in #11098 that will give users a way to opt-into advanced control over the execution of loaders and will enable implementation of middleware as well as this type of API in user-land 😄.

That's great. I haven't fully digested how that is going to work yet. So long as this doesn't force everything to be sequential. We still want loaders with no dependencies to be able to make network requests in parallel. In relay, while you often have a "single fetch" i.e. query, although you can have multiple (actually two is possibly the most common as you have one for the navigation which almost never reloads and then one for the page). It looks like the dataStrategy supports that but I've only had a quick look. I appreciate you considering this use case.

@steinybot
Copy link
Author

Ok yeah so I think I get it. The descendent route could say which ancestor route it depends on in its context. The dataStrategy could then use this to do a similar thing I did here in this PR. It would resolve the ancestor and then pass that result to the dependent loader via its context. Seems like it ought to work.

@olee
Copy link

olee commented Mar 21, 2024

@brophdawg11 could you maybe provide some insights how the new unstable_dataStrategy would allow depending on parent route data?

Would it be something like this maybe?

const parentRoutePromises = new Map<string, Promise<any>>();

export function getParentRoutePromise(id: string) {
  return parentRouterPromises.get(id);
}

function defaultDataStrategy(
  opts: DataStrategyFunctionArgs
): ReturnType<DataStrategyFunction> {
  return Promise.all(opts.matches.map((m) => {
    const promise = m.resolve();
    if (m.route.id) {
      parentRouterPromises.set(m.route.id, promise);
    }
    return promise;
  }));
}

While I think that this API does indeed allow for userland implementation of such a feature, I still think that providing an official way to get the promise of parent routes would be a great addition to the RR api.
Especially when developing SPAs instead of working with SSR, data dependencies are not that uncommon and as bad as with SSR and many people have requested such a feature before.
Also as described above, because the API would only return a promise, it would not automatically create any kind of waterfall the user doesn't intend to have because he has to intentionally not only call the API to get the promise, but actually await it inside a loader as well which would make it clear that this would make the child loader wait for some other data to resolve first.

PS: When can we expect data strategy to become available in a RR release? Would really like to try working with that.

@brophdawg11
Copy link
Contributor

The simplest way is to just call you loaders sequentially and collect the data along the way and pass it to the children:

function sequentialDataStrategy({ matches }) {
  let results = [];
  let loaderData = {};
  for (let match of matches) {
    let result = await match.resolve(async (handler) => {
      let data = await handler(data);
      data[match.route.id] = data;
      return { type: "data", result: data };
    });   
    results.push(result);
  }
  return results;
}

Your approach would also presumably work if you wanted to kick them off in parallel but then await internally to trigger the waterfalls - you would want to create a new Map on each execution of dataStrategy though so you're not using promises from prior navigations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Feature]: Parent loader data availability in child loader
4 participants