-
-
Notifications
You must be signed in to change notification settings - Fork 10.3k
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
[wip] experimenting with the new suspense hooks #7010
Conversation
you don't want to create a new resource on each preload you want to reuse a resources and its cache you want something like relay preloadQuery (https://relay.dev/docs/en/experimental/api-reference#usepreloadedquery), where you going to preload data and consume it using resource.read on render useResourcesPending could have another name, like isPendingTransition |
It's a hook, so it should be prefixed with |
usePendingTransition |
Very cool, thanks for sharing :) I believe
|
you can solve request waterfall of nested routes using Entrypoint approach (facebook/relay@861990a) similar to matchRoutes (https://github.com/ReactTraining/react-router/blob/master/packages/react-router-config/README.md#matchroutesroutes-pathname) where you could preload all data dependencies for each nested routes before rendering them |
I might be wrong, but currently we are call |
To be clear, just going for an API design here, the implementation has many problems and some new stuff we've been working on will enable us to fix them if we go this direction. |
@MeiKatz We're not trying to wrap the suspense API in something "better", just providing a way for the parent to fire off fetches on route changes. The demos in the docs do this on button click, we do it when matching routes, but we're not replacing any suspense APIs here.
It's not our job to "wait" for the data, the app gets to decide, that's what Suspense is all about! |
Based on @josephsavona's talk at React Conf this morning, we're thinking the API for integrating with Relay's new experimental API could be as simple as: // First, in your route.preload you need to preloadQuery
<Route
path="users/:id"
preload={params => preloadQuery(usersQuery, { id: params.id })}
>
<UserProfile />
</Route>
// Then, in your route component, use that data
function UserProfile() {
let user = usePreloadedQuery();
// go for it!
} If Apollo follows suit, we'll be able to integrate with them just as easily. |
@mjackson How does that prevent waterfall? Say you've got a user profile with tabs for their posts and friends. You go to a URL like If you load up that URL, you want to start a single (or at least simultaneous) data fetch to get the user and their posts. If you click to the friends tab, you want to make a fetch to get just the comments, as you already have the user data. Luckily, that's already handled. There needs to be a way to inform tree ancestors that you need to "add on" to the preload. We do this on React Redux with a Subscription class that allows you to have a parent subscription (all the way up to the root subscription on Would that pattern be a potential solution to that problem? |
what about nested routes? |
nice example relayjs/relay-examples#104 of how router works together with suspense |
I really like the idea of adding a First, I have always found the JSX-based route config very difficult to reason about. The object-based route config style supported by react-router-config matches my mental model so much better, so I just started there. For each entry I defined a // NOTE: this approach is problematic - see issues below!!
function App() {
const location = useLocation();
const[currentLocation,setLocation]=useState(location);
if (location.pathname !== currentLocation.pathname) {
setLocation(location);
}
const pathname = currentLocation.pathname;
// the issue here is that memoization only kicks in *after* a component commits
// so the component/data preparation logic will keep happening until the tree commits
// this work should happen *outside* React, in the route change handler.
const {component, prepared} = useMemo(() => {
// route entries are:
// { component: JSResource
// prepare: params => mixed }
const matchedRoute = matchRoutes(routes, pathname);
// returns {component, prepared} where `prepared` is results of prepare(routeParams)
return prepare(matchedRoute);
}, [pathname]);
return (
<ErrorBoundary>
<Suspense>
<RouteComponent component={component} prepared={prepared} />
</Suspense>
</ErrorBoundary?
);
}
function RouteComponent({component,prepared}) {
const Component = component.read(); // may suspend/throw; otherwise returns component
return <Component prepared={prepared} />;
} There are a few problems though. First, BrowserRouter itself calls setState, but this can trigger a re-render that suspends (e.g. when RouteComponent read()s the route's component or when that component tries to read its still-loading data). More importantly, though, this has many of the complications of fetch-on-render. In concurrent mode, App might render multiple times before it commits, or render and never commit. Note that useMemo doesn't work until a component has committed - so in practice, the preloading work in the useMemo hook will happen multiple times - not ideal! Also, the entire routing context object changes frequently, even when the route itself hasn't changed. Again, it's hard to avoid this in concurrent mode bc the only place to store temporary state is the component, and that may not be recycled when components suspend. What I ended up doing is the following:
You can see all of this in the I certainly don't have full context on all the use-cases that react-router is trying to support and issues such as incremental adoption, but from a new-user and concurrent/suspense perspective, having the above approach baked in would be really nice to use. I just define each route entry w its component and prepare function, and then the component loading and prepare() happens once when the route changes (outside of React), and the component gets rendered with the prepared data passed as a prop. No need for context or |
I think what you're saying here is that the difference between your example and this one is that you're doing your preloading when the route first matches instead of when it renders, is that right @josephsavona? Or are you suggesting there is a problem with storing the current |
@mjackson Basically yes: the key distinction is that for predictable behavior in concurrent mode - and for efficiency in starting the fetch earlier - preloading would ideally happen in the event handler for the location change, not in render. There's nothing inherently at issue with storing the location in context. But since the preload has to happen outside of render, the preloaded results also have to be able on context. Note how the implementation in the PR I linked only preloads code/data in the router itself, and makes that result available to context. If only the location is available on context, you have no choice but to preload the location in render. |
Thanks for confirming, @josephsavona. We are on the same page. When we actually ship this feature we will not be preloading inside render 👍 I think this PR might be a bit misleading since we're essentially trying to hack the current router API to do what we want. But the demo you made has the right idea; match the routes, start preloading, then render. |
Since that we are going to have preloading, would it be possible to also have a feature like preload on hover for I know that it can be implemented in userland, but it would be better if it worked by just flicking a prop on a |
check how to implement preload on on this pull request |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
@ryanflorence Does it mean the suspense support is abandoned? |
I think they got enough info to do it. Don't worry :) |
@lukaspili @oallouch I think they got caught by their own pull request policy and the corresponding bot ;) |
This PR was an experiment, never supposed to actually be merged. It's ok the bot closed it for us. 😅 We are still working on multiple aspects of suspense integration in v6. You can follow along on the |
I like the In this case we can introduce any data fetching mechanism, or cache preloaded data in any level and serve it to presentation component via context or redux store |
React Router + Suspense
In order for React Router to support suspense we need a few pieces.
Convert classes to functions
We need to use the
useTransition
hook to allow suspense boundaries to actually suspend. While suspense will still work with our class components, they never suspend, they always go to the loading state.This will be a breaking change for
<BrowserRouter/>
and friends, so we either do it in 6.0 or we ship two versions of everything (Router
,BrowserRouter
,HashRouter
, etc.).Update history to replace calls to push while suspended
We already know this. When a user clicks a link, then clicks another link before the resources are done loading, they'll end up with ghost entries in the history stack--meaning they can click "back" and see an unexpected page.
But we can do more!
Rather than just support it, we can actually help manage resources on route changes, and probably with a tiny API footprint.
NOTE: I'm just going for an API design idea here, the implementation has many problems that we can solve with stuff we've been working on but haven't released yet, so don't worry about the implementation too much.
Provide a simple entry point to preload data for the route
The React team's experience with suspense has shown that using render to kick off fetches in the component that needs the data leads to too many waterfalls. While they had hoped people would preload in some top-level component, they didn't. It would be unfortunate if suspense actually caused longer load times because the API encouraged waterfall requests.
The vast majority of data fetches in an app are initiated on route changes, so we're in a great position to help developers out. In fact, I was able to implement a very quick proof-of-concept in a few minutes, check it out:
<Route preload={(params, location) => resource} />
This prop will be called on render whenever a route matches and mounts. It passes in the params and location and expects a resource of any shape to be returned. It's up to the app to determine the api for the resource, we just provide a way to kick of a preload of it.
It might look something like:
So we might have a route like this:
useResource()
This hook is used inside a component to get access to the resource returned in the
<Route preload/>
prop. It will find the nearest resource and return it. Route components can use this hook to read data from the resource. Continuing with the invoice example:useResourcesPending()
This indicates that resources are being loaded. While suspense has a timeout before falling back or transitioning to a partial page, sometimes you want to add loading indicators to the first page. The
useTransition
hook returns anisPending
value. This simply gives route components access to it.That's it! Check out the demo in /fixtures/suspense