Skip to content

Instantly share code, notes, and snippets.

@redbar0n
Last active March 25, 2024 04:46
Show Gist options
  • Save redbar0n/f6ec12264ff9d58e243cc516e4e3f41b to your computer and use it in GitHub Desktop.
Save redbar0n/f6ec12264ff9d58e243cc516e4e3f41b to your computer and use it in GitHub Desktop.
Routing ideas

a thread where we can discuss some crazy routing ideas a bit out in the open branching out from the previous discussion on filesystem routes over at: https://discord.com/channels/815937377888632913/1014946079965454366

so, first of all, it seems there is a close alignment between how vite-plugin-ssr and router5 handles routing:

I recommend watching this router5 talk from 2:45 - 7:50 https://youtu.be/hblXdstrAg0?t=165 since it's really key to how I think about view / state separation, and I think of routing as a state separate from the view. (After all, and especially in a client-rendered app, what page it is showing is certainly a major part of the app's state.)

(I think this opens up powerful ways of both controlling and modelling routing, with tools like XState, via xstate-router and xrouter or with something simpler like mobx-state-tree, or any other tool of choice.)

That's the background.. so, I was thinking about nested routing (on the client)... React Router, router5 and vite-plugin-ssr all seem to operate from the principle that you need to switch on some condition to swap out parts of the page, so only those parts will be rendered... React Router uses components to achieve that switching, and router5 and vps uses if-conditions / ternaries.

But here's my (React-specific) question:

If React already does VDOM diffing to only render the parts of the view (component hierarchy) that changed.. couldn't one leverage that, to avoid having manual code (<Route> components) that switches out the correct parts of the page?

As you know, React will re-render a component when its props or state changes. So for instance, by sending in a prop, that changes when the route changes, thus automatically triggering a re-render of the component that represents the partial page:

<EditInvoice showWhen={currentRoute.invoices.invoice.edit} />

where the route object passed in will only have a non-null value for a certain route at the point that route is the current one.

The goal would be to:

  1. let React implicitly do the work of swapping out the parts of the page that changed. Don't do React's job, even partially: "any time your data changes, just blow away your [entire!] view, and re-render it from scratch, that would be so much easier. It would make the structure and amount of code in your app so much simpler ... "describe what your view looks like, and then never worry about updating it" - Tom Occhino introducing React in 2013.
  2. have a slightly cleaner looking component hierarchy (no visually disturbing components or interspersed conditionals),
  3. treat React simply as a "dumb" view layer.

In all, since React is the render library that was created with the aim of letting developers "simply re-render everything (and let React figure out the diff)", it seems weird to take that responsibility away from React (like specifically React Router does). React Router example:

<Routes>
  <Route path="/" element={<App />}>
    <Route index element={<Home />} />
    <Route path="teams" element={<Teams />}>
      <Route path=":teamId" element={<Team />} />
      <Route path=":teamId/edit" element={<EditTeam />} />
      <Route path="new" element={<NewTeamForm />} />
      <Route index element={<LeagueStandings />} />
    </Route>
  </Route>
  <Route element={<PageLayout />}>
    <Route path="/privacy" element={<Privacy />} />
    <Route path="/tos" element={<Tos />} />
  </Route>
  <Route path="contact-us" element={<Contact />} />
</Routes>

Could perhaps become:

<App>
  <Home showWhen={currentRoute.index} />
  <LeagueStandings showWhen={currentRoute.teams.index} />
  <Team showWhen={currentRoute.teams.team.index} />
  <EditTeam showWhen={currentRoute.teams.team.edit} />
  <NewTeamForm showWhen={currentRoute.teams.team.new} />
</App>
<PageLayout>
  <Privacy showWhen={currentRoute.privacy} />
  <ToS showWhen={currentRoute.tos} />
</PageLayout>
<Contact showWhen={currentRoute.contact_us} />

Or perhaps even clearer (where the routes tree is matches the component tree... could it even be derived from it, so the showWhen prop is made wholly redundant?):

<App>
  <Home showWhen={currentRoute.index} />
  <Teams showWhen={currentRoute.teams.index}>
    <Team showWhen={currentRoute.teams.team.id} id={1}>
      <EditTeam showWhen={currentRoute.teams.team.edit} />
      <NewTeamForm showWhen={currentRoute.teams.team.new} />
    </Team>
  </Teams>
</App>
<PageLayout>
  <Privacy showWhen={currentRoute.privacy} />
  <ToS showWhen={currentRoute.tos} />
</PageLayout>
<Contact showWhen={currentRoute.contact_us} />

Then inside you'd render either the or a child, namely a component. ... So, inside each of these components we would have:

if (showWhen === null) return null;

Then we would have "an application’s UI as a pure function of application state." -- G. Rauch https://rauchg.com/2015/pure-ui#f1 Treating routing as state, like router5 does. How do we guarantee that only one route in the hierarchy is displayed?

  • Well, conceptually it would be similar to the good old show/hide from the jQuery days (except that when a component returns null it will be removed from the rendered markup, not merely hidden with css).
  • Ultimately, the router would bear the burden of ensuring that no two currentRoute paths can be non-null at any time. the router being located somewhere off the render tree, ofc, in a state manager of some sorts (MST, XState etc.)

So, to your question @brillout

»The tricky part is data fetching. How would data fetching of nested views work with your architecture?"«

The short answer is that data-fetching could be non-nested (without the waterfall problem of nested UI components making rapid successive fetches for data from the server), since the model/state is completely off the render tree.

Potentially centralized (like Redux, conceptually).

But more ideally (since we want co-location with components and code-splitting) as distributed state machines living with their components...

You could have a hierarchical state-machine (actually called a 'statechart') for each page, for instance, where components on that page would send events up to the page's parent machine.

Which opens up avenues for modelling routes (together with other kinds of state, and data fetching) in either mobx-state-tree (MST) or xstate-router or xrouter.

I think it could be really powerful.

I view it in two ways:

  • An app has 2 layers: The render tree, and the state tree.

  • Components could in a way be like micro-MVC's aka. MVC-widgets that consists of a small state machine (Model), which on state changes calls (Controller) functions (data fetching actions etc.), and also declares the component render tree (View).

maybe "state graph" is the better description than a "state tree". It maps well with XState which is able to model that graph as a chart, namely a statechart.

Unrelated to any of the above, but still related to routing: Typesafe Routing - How RedwoodJS does it, and Pathpida is mentioned: https://youtu.be/inGxAvxvoHc?t=3567

ts-routes could perhaps help here, it looks similar to the route object design i had in mind. It allows strongly typed parameterized routes.

so I imagine data fetching would be kept completely separate from rendering, so rendering would be done simply by passing a state (routes object through props) and done by a centralized store or a collection of decentralized stores (one in each component file).

Ideally, one could even ignore filesystem routing and infer the routes based on the render tree completely, and have conventions that infer the route URL based on the render tree:

<App>
  <Home/>
  <Teams>
    <TeamNew/>
    <TeamId id={1}>
      <TeamEdit/>
    </Team>
  </Teams>
</App>
<PageLayout>
  <Privacy/>
  <ToS/>
</PageLayout>
<ContactUs/>

you could extract the URL's from the component tree in several ways, e.g.: by a compiler, or by the server performing an initial skeleton render upon deploy.

related: RedwoodJS' thought process concerning using vite-plugin-ssr https://youtu.be/tHW7Gn6WCSc?t=3094 they like single-file routing, because it makes it easy to get a quick overview of all routes→pages in one file. https://redwoodjs.com/docs/router another benefit is that it could allow for easier type inference of URL path parameters (due to those being mere props on components).

with routing based on state changes the state change could also execute side-effects like route announcements for accessibility purposes: https://redwoodjs.com/docs/accessibility#accessible-routing

Brillout - 5 Sept 2022 at 8:40 AM: @redbar0n I'm finishing Telefunc's React Hooks, I'll then have a look at all this. (Spontaneous reaction: my biggest concern with anything that tries to decouple state out of React is that certain React features need the state to be managed by React. E.g. I wonder how a server-side would work, or how React's new render concurrency would work. But I didn't read everything yet, so you maybe I'm saying something rubbish here.)

thanks

React ought to be able to depend on external state and receive state change signals. Since that’s what a lot of Redux setups essentially do. Flux pattern, essentially. Suspense could be made an internal concern of components if one wants, by wrapping them in a ‘withSuspense’. Imho, that makes a lot of sense, since default loading messages/spinners are nearly always the same. For those cases one need a custom loading state one could export the ComponentCore and wrap it in as normally.

But this is a bit beside the point of routing, I think.

btw, on the server, by itself (without RSC's), doesn't make much sense, I think. Since the boundary (which is basically an async if-statement) is generally be streamed to the client, where the content within it would then later be dynamically loaded and streamed in, as Suspense is intended to be used. RSC's play into this by letting you load the JS (bundle, dependencies for the component) on the server, and simply stream the result to the client (instead of the JS bundle) [1].

If you simply want dynamic content along static content (but not load it dynamically on the client), you wouldn't use on the server at all, but simply fetch the content and load it in synchronously on the server. I think.

[1] More in detail on how RSC's plays together with Suspense:

"Thanks to Suspense, you have the server streaming RSC output as server components fetch their data, and you have the browser incrementally rendering the data as they become available, and dynamically fetching client component bundles as they become necessary." https://www.plasmic.app/blog/how-react-server-components-work#does-this-work-with-suspense-though

NextJS: "Like streaming SSR, styling and data fetching within Suspense on the server side are not well supported. We're still working on them." https://nextjs.org/docs/advanced-features/react-18/server-components#data-fetching--styling

In summary: "On the server, Suspense enables progressively streaming HTML. On the client, Suspense enables selective hydration." https://twitter.com/reactjs/status/1508847169905864707?s=20&t=0kFbQN0mXMuUgREQLq6Jbw

@redbar0n
Copy link
Author

redbar0n commented Jan 10, 2024

@ChrisShank Cool! I see you renamed xrouter to routtl. I'm currently looking into Vinxi, which allows you to compose several different routers together (client router, server router, ...). This conversation by Nikhil Saraf and Ryan Carniato (SolidStart) was very interesting, especially when they compared the isomorphic-router approach of Vike aka. vite-plugin-ssr vs. Vinxi's approach of making various routers completely detached.

What is a router? https://vinxi.vercel.app/what-is-a-router :

Routers are the core primitive of Vinxi. You can compose your app by combining multiple routers together.

Why routers? If you think about it, the whole web stack is a bunch of http routers. Your browser is a router, your SPA is a router, you server is a router, you public directory is a router.

A router is a specification for how a group of routes should be handled. It specifies all kinds of behaviour about the router in the context of conventions, bundling, etc. Lets take a look at the different parts of a router.

@redbar0n
Copy link
Author

Chicane is a type safe router which was one of the inspirations for TanStack Router.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment