Sideload / Parallel Loaders #6234
Replies: 2 comments
-
This suggests a third option: expose more of the router, route-matcher, and revalidator as framework concepts. This would allow for both the side-loading and the layout-composition approaches to be implemented as plugins rather than as framework code. |
Beta Was this translation helpful? Give feedback.
-
@jamesarosen asked me to post a comment from Discord with @ryanflorence in this discussion, which I'm happy to do. I should note a couple things first, though:
My personal conclusion as to this pattern, however, is the following: It's as broad as the browser and I'm not sure a use case fully covers what's needed. Sometimes this doesn't matter, but, in this case, I do have the creeping concern that, if this general idea is accepted, care should be taken to examine it from beyond any one use case. Personally, I see the issue here as a browser/platform limitation. Sometimes, you want all the "platform" benefits of a URL without actually having a physical URL in the address bar. In other words, I've come to view this problem as one of real estate. The browser only gives the developer one address bar to work with, so it arbitrarily limits the conversation. Remix is in a somewhat unique position to bridge the gap. What I've decided to do, in a very narrow way, is to employ the concept of a "logical URL." This means a URL that I can feed to my app outside of the address bar. It is tied to a resource route. As a URL, I can work with it like a URL without being limited to the browser's concept of "physical space." Put simply, I can now pair a physical URL with many logical URLs at once. As you can imagine, one positive is that I can take this concept as far as I want. For example, the resource route can pick its component and offer actions and loaders for it. It can go further, too. The resource route can allow for form-driven, or intent-driven, siblings/children. In this case, rather than an
There's a lot that can be done with this — using many of the same patterns as the Web and Remix, but without the physical limitations of a single dimension. The downside for me is that I have to find ways around Remix's expectations to make this work. In another world, a future version of Remix could allow a type of My take is that this general idea is the best path forward for "parallel routes," as the spirit of the API surface would remain constant and its flexibility would be greatly increased. Anyway, I leave all that to everyone else. For now, here's the conversation and code James asked me to post:
__pathless.tsx
loader() {
let data;
switch (formData.parsed.intent)
case 'revalidate': { data = revalidateLogic() }
default: { data = initialLoadLogic() }
return json(data);
}
export function usePathlessRevalidate(path: string) {
const fetcher = useFetcher();
const [run, setRun] = React.useState();
React.useEffect(() => {
if (run) {
fetcher.load({ intent: 'revalidate' }, { action: path, method: 'get' });
setRun(false);
}
}, [run, fetcher, setRun, path]);
return [run, setRun, fetcher?.data];
}
export default function() {
const initData = useLoaderData();
const [run, setRun, data] = usePathlessRevalidate('routes/__pathless');
...
return <Outlet context={{ data, setRun }} />;
}
routeSegment.tsx
...
export default function() {
const { setRun } = useOutletContext();
const handleClick = () => {
...
setRun(true);
}
// (Or push this final setRun to trigger when some other fetcher is complete via a useEffect or via useRemixFetcher, which takes callbacks. Ish.)
...
} Src: https://discord.com/channels/770287896669978684/770287896669978687/1101552612903485620 -j |
Beta Was this translation helpful? Give feedback.
-
Executive Summary
shouldRevalidate
offers fine-grained control for loader data, but it only operates at the route level. If an app needs fine-grained control over when a specific piece of data revalidates, it must isolate that data in a separate route. This results in engineers having to make a tradeoff between UX and DX: either the resources are isolated and cached independently or the route tree grows impossibly long.Problem
First steps
Imagine building an e-commerce site. At first, the
root
route's loader has just some global data in it:This is a small amount of data that rarely changes, so the default
shouldRevalidate
logic works well.Add some data
We get a new requirement: the page navigation bar should have a list of 5 trending items. The trending list is context-sensitive: if we're looking at accessories, it should be a list of accessories. We add that to the
root
loader:Since the list is category-specific, we now need to revalidate the
root
data any time the category changes:We can load this data in our navbar with
useRouteLoaderData
:Add more data
Over time, we get more requirements for global data: the page header also needs the user's recent orders, support tickets, toast messages, ...
Eventually, we end up with a fairly large
loader
object:UX up, DX down
As the
loader
has grown, the logic forshouldRevalidate
has gotten both increasingly complex and increasingly likely to returntrue
. Switching from one category to another causes the loader to revalidatesupportTickets
even though that data isn't category-specific. This puts extra load on our servers and adds delay to page transitions.Remix gives us an out: because
shouldRevalidate
is tied to a route, we can extract data that changes at a different rate to its own route. Let's start with trending products:Remix can now revalidate
/?_data={routes/_trendingProducts}
independently.We need to make one simple change to our
<TrendingProducts>
component to match:But now our app doesn't load, complaining that
undefined.map
is not a function. That's because_trendingProducts
isn't in the route hierarchy for any of our pages. So for any page that needs this component (which is all of them, since it's in the main navbar), we need to insert_trendingProducts
into the route hierarchy:And if we want to separate more root loader data out, the problem gets worse:
This is reminiscent of the React Context "Flying V":
Proposals
Proposal 1: Side Loads
Allow routes to declare a list of side-loads:
This will cause Remix to do the following:
RouteMatch
next
matches but didn't appear in any of thecurrent
matchescurrent
andnext
matches and itsshouldRevalidate
returnstrue
next
matchesProposal 2: Compacted Layouts
Because side-loads aren't in the path hierarchy, they can't participate in the React DOM hierarchy. Importantly, they can't declare context providers. It might be nice to be able to do this:
The problem, then, is how to shrink
_recentOrders._supportTickets._trendingProducts
down. Ideally, we could shrink them all the way to a single declaration within theroot
route:Beta Was this translation helpful? Give feedback.
All reactions