Redwood Router with layouts, context providers, etc

Not many seconds passed after I wrote the post above before I realized it’s still too limiting.

One thing it still doesn’t solve is this: Detect subdomain via router, or globally in pages or Cells

Another thing is, what if I want to pass props to my layouts? Can’t do that if they’re automagically wrapped around the pages.

Looks more and more like the easiest option would just be to go with React Router :man_shrugging: Either as-is, or build something on-top of it.

@Tobbe would you be interested in taking time to present and discuss this at the next Contributor’s Meetup?

  • Show your RFC and the issues it resolves
  • Then highlight the limitations

Helpful? And thoughts/suggestions otherwise?

Yes please :slight_smile:

2 Likes

And just a suggestion from someone who has been trying to follow this discussion over the last few months, I think it would also be useful for Tom to give a really brief 1-2 minute explanation for exactly why the router is set up the way it is (flat instead of nested) cause it’s something that gets briefly mentioned every now and then but it isn’t something that I’ve ever been able to fully grok.

2 Likes

My latest idea:

<Router unauthenticated="home" groups="main">
  <Route path="/foo" page={FooPage} name="foo" groups="foobarbaz" />
  <Route path="/bar" page={BarPage} name="bar" groups="foobarbaz" />
  <Route path="/bar/wizard/step1" page={BarWizOnePage} name="barWizOne" groups={['foobarbaz', 'barwizzard']} />
  <Route path="/bar/wizard/step2" page={BarWizTwoPage} name="barWizTwo" groups={['foobarbaz', 'barwizzard']} />
  <Route path="/bar/wizard/step3" page={BarWizThreePage} name="barWizThree" groups={['foobarbaz', 'barwizzard']} />
  <Route path="/bar/wizard/step4" page={BarWizFourPage} name="barWizFour" groups={['foobarbaz', 'barwizzard']} />
  <Route path="/baz" page={BazPage} name="baz" groups="foobarbaz" />
  <Route path="/baz/wizard/step1" page={BazWizOnePage} name="bazWizOne" groups={['foobarbaz', 'bazwizzard']} />
  <Route path="/baz/wizard/step2" page={BazWizTwoPage} name="bazWizTwo" groups={['foobarbaz', 'bazwizzard']} />
  <Route path="/baz/wizard/step3" page={BazWizThreePage} name="bazWizThree" groups={['foobarbaz', 'bazwizzard']} />
</Router>

Here’s the example Router code we ended up with in the 2020-12-15 core meeting:

const DarkLayout = (props) => {
  return <AppLayout {...props} theme="dark" />
}

<Router>
  <Set wrap={[MarketingLayout, HeaderLayout]}>
    <Route path="/home" page={HomePage} />
    <Route path="/pricing" page={PricingPage} />
  </Set>

  <Private unauthenticated="home">
    <Set wrap={[AdminContext, DarkLayout]}>
      <Route path="/admin/users" page={UsersPage} />
      <Route path="/admin/posts" page={PostsPage} />
    </Set>

    <Set wrap={(props) => <AppLayout {...props} theme="light" />}>
      <Route path="/utility" page={UtilityPage} />
    </Set>
  </Private>

  <Route notfound={NotFoundPage} />
</Router>

Slight detour: based on this nested router example, I wonder if behind the scenes there’s really only one tag, <Route> that can take all of these different props, and <Set> and <Private> are just synonyms that let you make the routes easier to read and nest here if you want. But for simplicity in the underlying router code it’s all just calling <Route> and it can, optionally, be nested.

So as far as the router’s “parser” is concerned, the routes could have been defined as:

<Router>
  <Route path="/home" page={HomePage} layout={[MarketingLayout, HeaderLayout]}>
  <Route path="/pricing" page={PricingPage} layout={[MarketingLayout, HeaderLayout]} />
  <Route path="/admin/users" page={UsersPage} context={AdminContext} layout={() => <AppLayout navbar={true} private unauthenticated="home" />
  <Route path="/admin/posts" page={PostsPage} context={AdminContext} layout={() => <AppLayout navbar={true} private unauthenticated="home" />
  <Route path="/utility" page={UtilityPage} layout={() => <AppLayout navbar={false} />} private unauthenticated="home" />
  <Route notfound={NotFoundPage} />
</Router>

Maybe that would simplify the situation in the source instead of maintaining separate functionality in <Route>, <Set> and <Private> tags? Maybe?

1 Like

That’s the way it currently works. The router code loops through all children of <Private> and adds private to them as a prop, and then it removes <Private>. So you just end up with a single list of <Route> components

If I want a context that wraps all my routes, do I add context={MainContext} to <Router> at the top, or do I put a <Set context={MainContext}> just below it that wrapps all other <Set>s and all <Route>s?

There is nothing really special about Layout or Contexts, right? So, instead of

<Set layout={[MarketingLayout, HeaderLayout]} context={MainContext}>

couldn’t we just do

<Set wrapperComponents={[MainContext, MarketingLayout, HeaderLayout]}>

That way we wouldn’t have to add any new props if we wanted to add something like transitions (like this https://reactrouter.com/web/example/animated-transitions )

<Set wrapperComponents={[MainContext, MarketingLayout, HeaderLayout]}>

Ooooo interesting…I’m thinking we’d need a more concise name, but the idea is intriguing… /cc @mojombo

I wonder if behind the scenes there’s really only one tag, <Route> that can take all of these different props, and <Set> and <Private> are just synonyms that let you make the routes easier to read and nest here if you want.

@rob Ooh, I like the idea of being able to set the invidual props on the <Route>s or set them in an enclosing <Set>. That is conceptually very simple and gives some flexibility. What happens if both a Set and a Route define a prop? I guess the Route wins, yes? Maybe it’s a warning too, as that could get pretty confusing?

There is nothing really special about Layout or Contexts, right?

@Tobbe Good point! The only requirement is that they need to render their children. In that case, I suggest:

<Set wrap={MyLayout}>
<Set wrap={[MainContext, MainLayout]}>

We can also automatically send all these components the URL params, just like we do for Pages, so if they wanted to, they could use that stuff directly. Of course, they could also call useParams and friends.

We need to figure out how these are imported. Pages are auto-imported, which is also how they get code-split. Do we require you to manually import your wrappers? In that case, they will all get included in the main bundle.

1 Like

This is a good question. I’d want to auto-import them, but that does require us to know where in the filesystem they live. We can look for components named *Layout in /src/layouts/, but what if it’s <Set wrap={[MyLayout, MyAwesomeWrapper]}>, where do we find MyAwesomeWrapper?

It doesn’t have to be all or nothing. We can auto-import what we can, and require the user to manually import the rest. Like we can say “We autoimport all layouts and all contexts, the rest is up to you”. And even for things we have the users import themselves, they can still get code splitting, right? If they just put this at the top of the router file

const MyAwesomeWrapper = {
  name: 'MyAwesomeWrapper',
  loader: () => import('/src/wherever/MyAwesomeWrapper'),
}

Or they could even use React’s <Suspense> and lazy().

Going through one of my Redwood apps and I’m overwhelmed by scaffolded routes again and thinking of ways to consolidate. And this is only with three scaffolds! VS Code wrapping them doesn’t help, either:

<Router>
  <Route path="/" page={HomePage} name="home" />
  <Private unauthenticated="home">
    <Route path="/contacts" page={ContactsPage} name="contacts" />
    <Route path="/contacts/{id:Int}" page={ContactPage} name="contact" />
    <Route path="/settings" page={SettingsPage} name="settings" />
  </Private>
  <Private unauthenticated="home" role="admin">
    <Route
      path="/admin/contacts/new"
      page={AdminNewContactPage}
      name="adminNewContact"
    />
    <Route
      path="/admin/contacts/{id:Int}/edit"
      page={AdminEditContactPage}
      name="adminEditContact"
    />
    <Route
      path="/admin/contacts/{id:Int}"
      page={AdminContactPage}
      name="adminContact"
    />
    <Route
      path="/admin/contacts"
      page={AdminContactsPage}
      name="adminContacts"
    />

    <Route
      path="/admin/users/new"
      page={AdminNewUserPage}
      name="adminNewUser"
    />
    <Route
      path="/admin/users/{id:Int}/edit"
      page={AdminEditUserPage}
      name="adminEditUser"
    />
    <Route
      path="/admin/users/{id:Int}"
      page={AdminUserPage}
      name="adminUser"
    />
    <Route path="/admin/users" page={AdminUsersPage} name="adminUsers" />
    <Route
      path="/admin/preferences/new"
      page={AdminNewPreferencePage}
      name="adminNewPreference"
    />
    <Route
      path="/admin/preferences/{id:Int}/edit"
      page={AdminEditPreferencePage}
      name="adminEditPreference"
    />
    <Route
      path="/admin/preferences/{id:Int}"
      page={AdminPreferencePage}
      name="adminPreference"
    />
    <Route
      path="/admin/preferences"
      page={AdminPreferencesPage}
      name="adminPreferences"
    />
  </Private>

  <Route notfound page={NotFoundPage} />
</Router>

I haven’t build a Redwood app large enough yet, but my last major Rails app had 38 of them. That’d be 266 routes (!) if each one had to be declared literally! (Redwood equivalent would only be 152 routes.) Rails has the resources shortcut, can we do something similar with <Route>?

<Route path="/admin/users" scaffold="user" />

You’ve still got the path prop so it’s clear what the URL is to get to them and scanning through the routes still works.

You’re always free to use the more verbose route declarations if you want, but for the (probably more common case) of keeping the scaffold as admin pages, it removes a lot of mental overhead.

Look at Routes.js now, so tidy:

<Router>
  <Route path="/" page={HomePage} name="home" />
  <Private unauthenticated="home">
    <Route path="/contacts" page={ContactsPage} name="contacts" />
    <Route path="/contacts/{id:Int}" page={ContactPage} name="contact" />
    <Route path="/settings" page={SettingsPage} name="settings" />
  </Private>
  <Private unauthenticated="home" role="admin">
    <Route path="/admin/users" scaffold="user" />
    <Route path="/admin/contacts" scaffold="contact" />
    <Route path="/admin/preferences" scaffold="preference" />
  </Private>
  <Route notfound page={NotFoundPage} />
</Router>
1 Like

:100: RC. That makes a ton of sense.

With this, I could imagine users more likely to keep the generated scaffold code/routes. Otherwise, per your current Routes.js file, it seems people will be inclined to find their own ways to consolidate.

1 Like

That would be expanded to

<Route path="/admin/users/new" page={AdminNewUserPage} name="adminNewUser" />
<Route path="/admin/users/{id:Int}/edit" page={AdminEditUserPage} name="adminEditUser" />
<Route path="/admin/users/{id:Int}" page={AdminUserPage} name="adminUser" />
<Route path="/admin/users" page={AdminUsersPage} name="adminUsers" />

Looks like that could even be done at compile time, right? Using babel.

Only thing that stands out to me is the names. To “expand” to those routes, the scaffold would actually be called “adminUser”, right? And, since the actual page names are never shown anywhere when using your shorthand route, it might be a bit difficult for (new) users to know what name to use when linking directly to e.g. the edit user page, like <Link to={routes.adminEditUser()}>Edit User</Link>

I assume so, but are there drawbacks to that? If we ever released Redwood Router as its own separate package that you can add to any React app, does that make the integration more difficult?

I assumed it could work the same way that <Private> does, where is just adds more attributes to a regular <Route> (although it would also need to inject 3 additional routes, not sure about the feasibility of that).

For sure. We could help by displaying the routes and associated names after you run the scaffold generator (and maybe include a flag if you want the full route from the get-go):

User scaffold generated! To reference your routes by name:

  /admin/users -  List of all users
    routes.adminUsers()

  /admin/users/1 - Detail for a single user
    routes.adminUser({ id: 1 })

  /admin/users/new - New user form
    routes.adminNewUser()

  /admin/users/1/edit - Edit user form
    routes.adminEditUser({ id: 1 })

If you'd rather have the full routes written to your Routes.js 
file, use the `--verbose-routes` flag.

We could also include a comment above the shorter <Route> right in the Router file that links to a quick doc about naming conventions.

It ends up being a simple naming convention so once you have it described to you (and throughly documented) I think people will get it. Rails has had this forever and I don’t remember seeing any blow back in the community.

1 Like

Hah! And here I was thinking "Why isn’t <Private> done with babel as well?!

The main reason to do it with babel would be to not spend cpu cycles doing it (over and over again) at runtime. But yeah, as with everything related to optimizations - should measure first!

React.createElement is the answer to that I think.

Update

Tom, Peter, Rob, and I had a solid discussion about this today and agree it’s ready for moving to implementation of a POC with goal to resolve fully for v1. :rocket:

Next Steps

  • Peter is creating a GitHub Issue to outline decisions (from this thread and today’s discussion), plan, and next steps. The conversation should move from here to the Issue when it’s posted.
  • Tobbe has confirmed he’d like to take lead on implementation in coordination with Peter.
  • Go Team :rocket:

And huge thanks to everyone who helped get us this far. This is a critical piece to complete for v1.

1 Like

@Tobbe Updated the code example in Redwood Router with layouts, context providers, etc to reflect what we talked about in our meeting. It shows two ways to pass props to your Layout, one creating a standalone function and the other doing it inline:

const DarkLayout = (props) => {
  return <AppLayout {...props} theme="dark" />
}

<Router>
  <Set wrap={[MarketingLayout, HeaderLayout]}>
    <Route path="/home" page={HomePage} />
    <Route path="/pricing" page={PricingPage} />
  </Set>

  <Private unauthenticated="home">
    <Set wrap={[AdminContext, DarkLayout]}>
      <Route path="/admin/users" page={UsersPage} />
      <Route path="/admin/posts" page={PostsPage} />
    </Set>

    <Set wrap={(props) => <AppLayout {...props} theme="light" />}>
      <Route path="/utility" page={UtilityPage} />
    </Set>
  </Private>

  <Route notfound={NotFoundPage} />
</Router>
1 Like