Replies: 13 comments 29 replies
-
I definitely prefer this approach over #5383. But I find it somehow confusing to identify the relationship between siblings and the parent when looking at the file paths. My first impression was that the This makes me wonder if it would be cleaner to swap the order you defined the named slot:
With this you can identify where the named outlet is (i.e. <Route path="dashboard">
<Route path="articles" />
<Route path="users" />
<Slot name="left">
<Route path="articles" />
<Route path="users" />
</Slot>
<Slot name="right">
<Route path="articles" />
<Route path="users" />
</Slot>
</Route> |
Beta Was this translation helpful? Give feedback.
-
I love this. I think it is really elegant how Remix can analyse the entire dependency graph for every route (both static assets and data requirements) either by:
By looking at this graph (in config or on the file system) you can see at one glance, a lot about the structure and complexity of the app. The direction of #5383 is powerful, but completely breaks this mental model of Remix. Somewhere buried in components lays the definition of API routes. What I love about this proposal is that it feels completely inline with Remix, but just improves the dependency graph to make it endlessly more powerful for complex data requirements. You can now split any route in as many little isolated view/data/mutation modules as you want. I would only change one thing. I think it might be more natural if the "main sibling" (the one that has a path) would be in charge of placing the "named" siblings. In your proposal the parent of the "siblings" is in charge of placing the child and its siblings, but not every child may have the same named siblings, and I can imagine that the placing of the siblings depend on the specific child. For example, say, I have two layout routes: routes/
_landing.about.tsx
_landing._index.tsx
_landing.tsx
_landing@header.tsx
_landing@footer.tsx
_landing@sidebar.tsx
app._index.tsx
app.projects.tsx
app.tsx
app@nav.tsx
app@sidebar.tsx Now in the proposal, the root route (root.tsx) would have to determine where to place the siblings, of both // ~/routes/_landing.tsx
export default function () {
return (
<>
<Outlet name="header" /> // _landing@header.tsx
<div className="flex flex-row">
<Outlet />
<Outlet name="sidebar" /> _landing@sidebar.tsx
</div>
<Outlet name="footer" /> // _landing@footer.tsx
</>
);
} // ~/routes/app.tsx
export default function () {
return (
<div className="flex flex-row">
<Outlet name="nav" /> // app@nav.tsx
<Outlet />
<Outlet name="sidebar" /> // app@sidebar.tsx
</div>
);
} Another example:
Here, only the I think this would also solve @edmundhung concern. |
Beta Was this translation helpful? Give feedback.
-
Jamie Kyle had an idea for slots that reminds me of this: |
Beta Was this translation helpful? Give feedback.
-
What would be the revalidation behavior when a sibling is initiating a mutation (action)? Here are our use case: We are currently "remaking" a very large website (B2C) with React Router > 6.4 and all its philosophy and plan to gradually migrate to SSR with Remix. Some pages can be composed of a lot of small components (derived from an internal Design System). Each of those components declares its own data dependency with GraphQL fragments (colocation principle). Since the page is behind a single route (path) and a single loader, it's really difficult to control atomically each sub-component data loading & mutation behavior, and mainly data revalidation. ATM, the route loader is responsible of executing all the queries separately for all the page sub-components in parallel (with Apollo Client) while deferring the resulting data promises. Since everything is behind a single route and thus a single loader, if a sub-component initiates a mutation (so executes the route action), all queries to the GraphQL server would be executed again when React Router revalidates the route loader data even though the sub-component initiating the mutation is the only one concerned by the changing state. As an example, say we have a sub-component in the page with a "like" button. We would use a fetcher to initiates an action on the route path, and would optimistically update the button to the "liked" state while the server is processing the request. Then we would need to revalidate only the data for this specific sub-component, thus only this sub-component loader function should be executed on revalidation. How would this happen with this proposal? By implementing a For now, we have to rely on Apollo Client cache, so when a sub-component is mutating data, the route loader is executed again on revalidation, so all queries for all sub-components are executed again, but since Apollo already has data in cache, it doesn't make new requests to the server... convenient but a bit magical :) |
Beta Was this translation helpful? Give feedback.
-
I have a use case for this in my workshop app I'm working on: This feature would allow me to separate the data loading requirements of those preview tabs from the main route for the page which would help me separate concerns. I'm in favor 👍 |
Beta Was this translation helpful? Give feedback.
-
We had named outlets in early versions of React Router, I think they could be really interesting to bring back, especially with all the data abstractions we have now. I don't think, however, they are a replacement of #5383. One major difference is that this expects all child routes of a parent route to share a set of siblings coupled to the parent layout. While this helps get some data more granular and co-located, the abstraction is not a general way to do it: it only works when child routes share a set of "sibling" routes. When they don't share the same set, the parent route gets overly concerned and aware of every child route's siblings. This isn't a critique of the proposal, but the main reason I don't think it's a replacement for the other. I've been working on a couple other ideas that go along with #5383 that I'll be sharing soon that will hopefully share the problem and scope I'm trying to solve over there. I think this proposal should be considered independently. |
Beta Was this translation helpful? Give feedback.
-
I was thinking what if this was modeled around the URL fragment identifier instead. If it makes any sense and there's interest I can write up a proper proposal. We could call it Hash Routes since it is modeled on the URL fragment Hash Routes
React Router APIconst router = createBrowserRouter([
{
path: "/",
element: <Root />,
loader: rootLoader,
children: [
{
path: "team",
element: <Team />,
loader: teamLoader,
children: [
{
path: "#tweets",
element: <Tweets />,
loader: tweetsLoader,
action: tweetsAction
},
],
},
{
path: "#header",
element: <Header />
loader: headerLoader,
},
{
path: "#footer",
element: <Footer />
loader: footerLoader,
},
],
},
]); Remix APIroutes/#header.tsx
routes/#footer.tsx
routes/team/#tweets.tsx
routes/team.tsx
Hash Route LoadingSince hash routes are leaf nodes in the route tree, there could be opportunity to lazy load them individually based on viewport, media query or browser idle e.g. no point loading a google maps component and paying API fees if it is below the fold. The API to achieve this would need to be determined. I'm also curious if hash routes could stream their html directly from the server and not send a JS bundle in Remix by the loader returning If these are possible then this would be a much simpler alternative to a lot of the benefits and use cases for React Server Components. Hash Route OutletsThe parent route would use Outlets to place the hash routes in the desired positions. <Outlet href="#tweets" />
<Outlet />
// Conditional Routes
{tweetsEnabled && <Outlet href="#tweets" /> }
<Link href="#tweets" prefetch="intent" />
|
Beta Was this translation helpful? Give feedback.
-
Plus one! I've been wanting this for a while now. I think this proposal is better written (and up to date) than mine was back then, but I still wanted to loop in this discussion/suggestion from over in the React Router repo which suggests mostly the same API. |
Beta Was this translation helpful? Give feedback.
-
I opened #6234 before reading this. I do think they're quite similar, but I wanted to point out one important difference. As far as I can tell from your proposal, it's not possible for a sibling / slot route to declare a React context provider for the layout's children: // ~/root.tsx
export default function Root() {
const { recentlyViewedProducts } = useRouteLoaderData('routes/_index@recentlyViewedProducts')
return (
<RecentlyViewedProductsProvider products={recentlyViewedProducts}>
<Outlet name="recentlyViewedProducts" />
<Outlet />
</RecentlyViewedProductsProvider>
)
} There are cases where the sibling route is the primary but not sole UI for its data. For these use-cases, the primary benefit of isolating it in a sibling route is being able to optimize HTTP caching; the UI isolation is secondary. |
Beta Was this translation helpful? Give feedback.
-
Has the team provided any updates on this, considering it has already been a year? I am kinda in need of a named outlet. Alternatively, functions like |
Beta Was this translation helpful? Give feedback.
-
Does this solve the modal route use case? Imagine you have a modal that can be opened anywhere, it doesn't matter how nested the route you're in. For example, you have /calendar, /messages, /messages/1... and the modal route would match /calendar/preferences, /messages/preferences, /messages/1/preferences and so on. For to modal to close, you'd just put navigate(-1) and the user would be back right where it first opened. Ideally, you'd want to keep the layout as well (as backdrop in this case). Or can this be achieved already? |
Beta Was this translation helpful? Give feedback.
-
I actually have a different use case for this feature: I'm creating a dashboard which aggregates data from 3 different apis. two of them have limits, so I want to call API 1 every minute, API 2 every 5 minutes and API 3 every 30 minutes. to me it seems not possible to achieve that nicely with remix at all. I would so much love to be able to use |
Beta Was this translation helpful? Give feedback.
-
Will the parallel routes feature be included in RR v7? |
Beta Was this translation helpful? Give feedback.
-
Proposal
Add sibling routes support to Remix and React Router to allow matching multiple routes in the same segment but being independent chunks and with a different data URL.
Background
The proposal #5383 was created to discuss the possible to add some compiler magic to let component define a loader and Remix compiler will generate a route for it and call it together with other loaders (including the route loader) on each route where this component is imported.
After discussing in Discord about this, I came to the conclusion that a simpler and less magical way to achieve a similar result would be to support sibling routes.
The idea would be that any route could have sibling routes that have their unique component, loader, action, error boundary and links, and both the route and its sibling will match the same route segment.
This will allow you to split a UI in different components (a main one plus multiple siblings) and render them together, with their own data loading and mutations.
Use Case
API/Examples
React Router API
In React Router a way to define them could be by using a
siblings
attribute in any route element:Note that
siblings
is not an array of routes but an object, this is because each sibling must have a name and the name must be unique between all siblings.A sibling is an object similar to any other route except it doesn't include a
path
orchildren
attribute.The
createBrowserRouter
function should update its type to ensure the first list of routes can't have siblings, so if right now there's aRoute
interface defining what route properties can be set, then there should be aRouteWithSiblings
interface that extendsRoute
and adds thesiblings
attribute.This should ensure that routes without a parent can't have siblings, because if you did it the siblings would never be rendered.
Remix File Convention
I propose using
@
as a delimiter for sibling routes in Remix, for example in the v1 convention:Or in the v2 convention:
Both conventions will generate the route
/parent/leaf
.Named Outlet
The parent route will have to render an
<Outlet />
for the leaf route, and another one with a name for each sibling.For example, if the parent route renders a three column layout, it could render the leaf route in the middle column and the sibling routes in the left and right columns.
This was, it would be possible to build a more complex layout by using siblings, if the layout has multiples leaf routes each one could provide a different left and right sibling.
These route files would generate the following URLs:
To give give a more real-world example the following image is a screenshot of Daffy's dashboard.
As you can see, we have multiple nested routes, but the middle and right columns are part of the same route, we had to use a trick with the
handle
export anduseMatches
to let the parent route render the right sidebar in the layout.By using sibling routes we could have a much simpler implementation by moving the right sidebar to a sibling route.
We could take this a step further and split the sidebar in multiple sibling routes, one for each widget in the sidebar to be a route by itself with its data.
If an Outlet is rendered with a name for a sibling route that doesn't exist, it should render
null
. By doing this we can have a more flexible layout that can be rendered in different ways depending on the sibling routes that exist.For example the Home Leaf Route could have the siblings mentioned on the image, but a different leaf route could have a different set of siblings.
The app layout route will only need to know where to render every sibling, it doesn't need to know what siblings exist so no all leaf routes needs to have the same siblings.
Matches
The
useMatches
hook would also need to be updated to support siblings, I think the simplest way is to follow a similar pattern to the RR API and add asiblings
record to the matches object.The type definition would be something similar to:
The return value of
useMatches
will beMatchWithSiblings
, and siblings are simpleMatch
objects meaning they don't have a siblings property.useRouteLoaderData
A sibling route data should be available through this hook by using the route ID as any other route.
In this case the route ID of a sibling route will include the
@sibling-name
part at the end.Reusability
If you want to use the same component as a sibling in multiple leaf routes you could use a simple re-export to avoid duplicating the code.
You could even move the definition outside routes.
This way you can treat the sibling route as a normal component as the #5383 proposal suggests, but there's no magic to convert it to a route.
While this is more manual work than the other proposal, it's more obvious what's happening without being magic.
Data URLs
The data URL for a sibling route will follow the same conventions used by Remix already.
This will allows sibling routes to set Cache-Control on the loader response and get them cached individually. For the Daffy example, the "Make a Donation Sibling Route" that has a search form, could submit a GET request to the route itself, and I could cache the results returned by the loader with a different cache policy than the rest of the routes.
Data Revalidation
Because sibling routes can affect the URL and are affected by changes there, the
shouldRevalidate
API becomes way more useful.A sibling route like the "Make a Donation" one could make the search form change the URL search params, because I know the search param used by that route is not used by any other route, those routes could include a
shouldRevalidate
function that returnsfalse
when that search param and only that one change.Similarly, the "Make a Donation" route could have a
shouldRevalidate
function that returnsfalse
when a different search param changes because they don't affect the results.This gives the developers using Remix a lot of flexibility to decide what to revalidate and when, by splitting a route in more granular sibling ones.
Links Function
The
links
function should work as usual, any sibling route can export it and they will be combined with the rest of the links returned by other sibling routes, leaf routes and layout routes.Headers Function
The sibling routes should be able to export a
headers
function, that will work as usuall by allowing the route to know the loader, action and parent headers and return a new group of headers.For non sibling routes, the route will receive a
Record<string, Headers>
with the headers of all sibling routes.This way, the leaf route can receive the headers set by its siblings loaders and combine them.
This record of sibling headers will be empty on sibling routes.
Meta Function
This will be similar to Headers Function, the sibling routes can export a
meta
function and the non-sibling route will combine them.Siblings in Layout Routes
Layout routes could define their own siblings to let the layout of the layout render them. However, sibling routes can't have nested routes, this means
routes/dashboard@sidebar.settings.tsx
is not a valid route andremix build
should fail.However this file structure must work:
In that case, the
routes/dashboard@sidebar
route will be rendered by theroot
route as a sibling ofroutes/dashboard
.Open questions
Prior Art
Beta Was this translation helpful? Give feedback.
All reactions