The current v6 preload proposal appears difficult to statically type. Here is a proposal for an API that would be easier to type correctly. I implemented a small proof-of-concept using Flow (we don't use TypeScript) and everything looks good.
I know the v6 preload API is still very early, but I'd like to contribute some thoughts to the discussion. If I'm understanding correctly, the proposal for the current preload API looks something like this:
import { Route, useResource } from 'react-router'
<Route
preload={routeParams => aResource}
element={<Component />}
/>
function Component() {
let resource = useResource() // access the return value from the preload call
...
}
We use static typing extensively in our application and I'm struggling to envision a way to type the return value of useResource without resorting to an ugly type cast that is not statically verifiable.
Here is an example API that would be much easier to statically type:
import { Route } from 'react-router'
<Route
preload={routeParams => aResource}
element={resource => <Component resource={resource} />}
/>
function Component({ resource }) {
...
}
The element prop accepts either a React element or a function that accepts the resource (returned from preload) and returns a React element.
To avoid prop-drilling, we could also pass the resource through context:
import { Route } from 'react-router'
<Route
preload={routeParams => aResource}
element={resource => (
<Resource.Provider value={resource}>
<Component />
</Resource.Provider>
)}
/>
function Component() {
let resource = useContext(...)
// could also accept resource as a prop and render the provider in here
}
Here is a full example inspired by some of our application code, including our take on the "render-as-you-fetch" pattern. The application code is from a chat UI.
import { queryCache, useQuery } from 'react-query'
function fetchThreads() {
// threads fetching code
}
function prefetchThreads() {
queryCache.prefetchQuery(fetchThreads)
}
function makeThreadsResource() {
prefetchThreads()
return function useThreads() {
// This hook is embedded and returned here so that it is
// inaccessible outside a call to makeThreadsResource().
// This enforces the "render-as-you-fetch" pattern.
return useQuery(fetchThreads).data
}
}
<Route
preload={() => ({ useThreads: makeThreadsResource() })}
element={resource => <Threads {...resource} />}
/>
function Threads({ useThreads }) {
let threads = useThreads()
...
}
I would be happy to share my proof-of-concept Flow typings if they would help.
preload function would need to be written so that it is callable multiple times. At minimum, it would need to be called during the route's render so that its return value can be passed just-in-time to the element function (if it is a function and not a simple element). The application code inside the preload callback would take responsibility for caching network calls, etc.preload could be triggered by a customizable list of events, such as 'mousedown' on an associated <Link>. In that case, I could conceive of the preload function being called at least twice: once inside the mousedown handler and again when the route actually renders.preload callback is NOT the place for that effect. A better place would be an onEnter callback like RRv3.also, one more thing.
imagine we have a route config like this:
[
{
path: "/",
element: Foo,
preload: fooResource,
routes: [
{
path: "/"
element: Bar,
preload: barResource
},
{
path: "/baz"
element: Baz,
preload: bazResource
},
{
path: "/qux"
element: qux,
preload: quxResource
}
]
},
{
path: "/other",
element: Other,
preload: otheResource,
}
]
if we move from /other to / we need to fetch fooResource and barResource and with current API of react-router-config this is possible.
but if we move from / to /baz we only need to fetch bazResource and not fooResource, there is no API for this in V5 and I think having an API for this is really useful...
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
You can add the fresh label to prevent me from taking any action.
Thank you! Just implementing a Relay solution and I agree with the above. I would be great to have access to the return value of preload.
Stumbled upon this while I'm researching how to implement a deferred page transition and loading indicator, eg. waiting for the lazy component (or some other data), before actually executing the page switch. As I understand it's currently not possible at all in the v6 beta version without preload and useLocationPending, right?
@maggo If you use react experimental you can use Suspense and useTransition. This works with react router 6 but does not preload (e.g. load on mouseover or so). You can show a loading indicator before navigation though.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
You can add the fresh label to prevent me from taking any action.
I didn't have chance to use the new react-router with preload yet, but if I had, I would probably do it like this:
import { queryCache, useQuery } from "react-query"
function fetchTodos() {
return fetch("/todos").then(res => res.json())
}
function App() {
return (
<Routes>
<Route
path="todos"
preload={() => queryCache.prefetchQuery("todos", fetchTodos)}
element={<Todos />}
/>
</Routes>
)
}
function Todos() {
const todos = useQuery("todos", fetchTodos).data
return …
}
@phaux Yeah, I like this. But it should also pass the route params as argument of the callback function.
@phaux I've been trying to do something really similar with Relay experimental, but I'm getting an error (Relay: 'loadQuery' (or 'loadEntryPoint') should not be called inside a React render function.).
If I understand this Relay error, somehow, the preload function is called during render, which shouldn't happen.
See this examples: https://github.com/Hellzed/hello-relay-react-router-experimental
It includes a "hello" GraphQL server for convenience, as well as a simple React + Relay experimental + React Router v6 experimental app. The Relay environment provider is located in the index module, while the router is in the App module.
@Hellzed
Not possible since cannot be called during render or by render itself... I think this is issue from react-router related to relay...
You can workaround this by wrapping custom router and call it in use effect out of router...
const query = graphql`
query AppHelloQuery {
hello
}
`;
function Hello({ queryReference }) {
const data = usePreloadedQuery(query, queryReference);
return <p>{data.hello}</p>;
}
function RouteWrapper({loadQuery,reference, ...rest}){
useEffect(() => {
loadQuery({})
}, [loadQuery])
return <Route {...rest}/>
}
function App() {
const [queryReference, loadQuery] = useQueryLoader(query);
return (
<Router>
<Routes>
<RouteWrapper
loadQuery={loadQuery}
path="/"
element={
<React.Suspense fallback="Loading...">
{
queryReference != null && <Hello queryReference={queryReference} />
}
</React.Suspense>
}
/>
</Routes>
</Router>
);
}
@damikun Now I see!
Since the purpose of the preload prop reads as follows:
The
<Route preload>prop may be used to specify a function that will be called when a route is about to be rendered. This function usually kicks off a fetch or similar operation that primes a local data cache for retrieval while rendering later.
Wouldn't it make sense for React Router v6 to always wrap the function passed to preload in useEffect or useCallback, since we're expecting it to trigger a fetch and/or a cache state update?
(Haven't tried it, but do you think there would be any adverse effect in wrapping this whole block in React.useEffect(): https://github.com/ReactTraining/react-router/blob/dev-experimental/packages/react-router/index.tsx#L662-L668 )
Most helpful comment
I didn't have chance to use the new react-router with preload yet, but if I had, I would probably do it like this: