Redwood Router with layouts, context providers, etc

Router RFC

I have finally come up with an idea that I think solves all my issues with the current router. And I hope it retains the original ideas behind the router design. I’m happy enough with this proposal that I’d like to try to implement it, if I get the go-ahead.

Basically I want to make Layouts a bit more magic and more central to how Redwood works.

Current issues

  • Layouts are re-rendered every time the page re-renders
  • There is no way to scope a context to a set of pages
  • It’s not clear what the difference is between layouts and regular components. Layouts can be used as components and components can be used as layouts, so why do we even have both?
  • We already have nesting in the router, so people is always going to ask for more nesting (see here for example)

This proposal tries to solve all those issues

Basic example from the tutorial

This is what the routes would look like for the tutorial:

<Router unauthenticated="home">
  <BlogRoute path="/contact" page={ContactPage} name="contact" />
  <BlogRoute path="/blog-post/{id:Int}" page={BlogPostPage} name="blogPost" />
  <PrivateRoute path="/admin/posts/new" page={NewPostPage} name="newPost" />
  <PrivateRoute path="/admin/posts/{id:Int}/edit" page={EditPostPage} name="editPost" />
  <PrivateRoute path="/admin/posts/{id:Int}" page={PostPage} name="post" />
  <PrivateRoute path="/admin/posts" page={PostsPage} name="posts" />
  <BlogRoute path="/about" page={AboutPage} name="about" />
  <BlogRoute path="/" page={HomePage} name="home" />
  <NotFoundRoute page={NotFoundPage} />
</Router>
  • This would give you a totally flat router. No more “Why can I wrap my routes in <Private>, but not in <MyFavoriteWrapper>?” questions.
  • The unauthenticated prop has moved from <Private>, which we don’t have anymore, to <Router> instead
  • Each <*Route> would load the corresponding layout automatically, and wrap the route in that layout. So <BlogRoute path="/about" page={AboutPage}> would automatically load <BlogLayout> and render AboutPage where {children} is in BlogLayout
  • <PrivateRoute> both defines the route as private, and also loads PrivateLayout (if it exists)
  • <NotFoundRoute> both defines this as the 404 route, and also loads NotFoundLayout (if it exists)
  • Except for the new way of specifying private pages and the 404 page this is 100% backwards compatible. If only <Route path="..." ...> is specified, no layout will be automatically loaded. And all layouts can still be imported and rendered in pages as they currently are.

More involved example from the first post in this thread

The tutorial project is pretty small and simple. My first post in this thread had a bigger, more complex example. This is what that example would look like

<Router unauthenticated="home">
  <PrivateRoute path="/admin/posts/new" page={NewPostPage} name="newPost" />
  <PrivateRoute path="/admin/posts/{id:Int}/edit" page={EditPostPage} name="editPost" />
  <PrivateRoute path="/admin/posts/{id:Int}" page={PostPage} name="post" />
  <PrivateRoute path="/admin/posts" page={PostsPage} name="posts" />

  <FooBarBazRoute path="/foo" page={FooPage} name="foo" />
  <FooBarBazRoute path="/bar" page={BarPage} name="bar" />
  <FooBarBazWizardRoute path="/bar/wizard/step1" page={BarWizOnePage} name="barWizOne" />
  <FooBarBazWizardRoute path="/bar/wizard/step2" page={BarWizTwoPage} name="barWizTwo" />
  <FooBarBazWizardRoute path="/bar/wizard/step3" page={BarWizThreePage} name="barWizThree" />
  <FooBarBazWizardRoute path="/bar/wizard/step4" page={BarWizFourPage} name="barWizFour" />
  <FooBarBazRoute path="/baz" page={BazPage} name="baz" />
  <FooBarBazWizardRoute path="/baz/wizard/step1" page={BazWizOnePage} name="bazWizOne" />
  <FooBarBazWizardRoute path="/baz/wizard/step2" page={BazWizTwoPage} name="bazWizTwo" />
  <FooBarBazWizardRoute path="/baz/wizard/step3" page={BazWizThreePage} name="bazWizThree" />

  <Route path="/" page={HomePage} name="home" />
  <Route path="/about" page={AboutPage} name="about" />

  <NotFoundRoute page={NotFoundPage} />
</Router>
  • Compared to the original example this version doesn’t have a <MainLayout> that wraps all the routes. This is solved by the router always loading RouterLayout if it exists (or whatever special name we decide on), and wrapping everything in that.
  • In the original example there was one FooBarBazLayout that had two WizardLayout as children. This is only partly solved in this new example. What I was thinking is that when we look for a layout to wrap the current route in, we don’t only look for an exact match on the whole name, but also for 0-index anchored sub-strings, and then load that layout first, if we find one. So in this case, when looking for FooBarBazWizardLayout we’d find FooBarBazLayout, so we’d load that, and then put WizardLayout inside, as a child.
    • What we don’t solve is that, in the original example, when you navigate from a page inside one group of WizardLayout routes, directly in to a page in the other group we’d re-mount the layout. Here everyone is children of just one instance of WizardLayout. If this was a real problem the user would just have to create two layouts, e.g. WizardOneLayout and WizardTwoLayout, who both probably are just basically reexporting WizardLayout. The user would then have <FooBarBazWizardOneRoute> routes and <FooBarBazWizardTwoRoute> routes
  • Contexts are not shown anywhere. They can be placed inside the layouts if needed.

I think there’s only one more thing I haven’t addressed so far, and that’s the use case I found where someone had two different layouts as siblings in a page. I think this is happening because 1. we’re currently importing and using layouts in pages. 2. we haven’t been super clear on how/where layouts are supposed to be used. I think if the tutorial showed this new way of working with layouts then that’s what people would do. And, as I noted earlier, if someone still wanted to do it like this, they totally could, and it would work just as well/bad as it does today.

I’m hoping this can all be done with babel, but I’ve never written a babel transform plugin, so I can’t be sure. Please tell me if this doesn’t look doable! Is it possible to keep VSCode happy about this? So that it doesn’t complain about using undefined components. I mean, there really isn’t a component called <BlogRoute>. I also have one more idea, that builds on top of this concept I’ve proposed here, but let’s take one thing at a time, eh? :slight_smile:

TL;DR

Layouts are automatically loaded based on route component names and this solves all our problems.

1 Like