Skip to content

Instantly share code, notes, and snippets.

@redbar0n
Last active March 25, 2024 04:46
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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 Sep 9, 2022

A problem was mentioned on Twitter:

What if you forget to connect all links to the correct routing events (aka. the redux-router trap)?

So, how about navigating with a <Link> component that will always send the correct event for you?

Based on the route state you specify it to go to. Something like:

<Link goTo={[currentRoute.teams.team.id]>

@redbar0n
Copy link
Author

redbar0n commented Sep 10, 2022

What I envision, is an architecture where:

  1. User actions in the View triggers actions on corresponding XState machine.
  2. XState machine changes the route state.
  3. React will automatically re-render the View based on the new route state. The routing ideas above here potentially illustrate a way to do that.

You don't need XState to directly drive the rendering in React (witth the likes of xstate-component-tree or xstate-first-react or similar). It suffices that XState changes the route state, which React picks up and uses to re-renders itself.

Rendering thus becomes a side-effect (like it always should have been...). Rendering ought to be the end-result, not the start of some other side-effects (I'm looking at you,useEffect).

@redbar0n
Copy link
Author

redbar0n commented Sep 15, 2022

FFR, reminder, taken from xrouter:

Routing responsibilities typically involve:

  • Deserializing/serializing the URL
  • Mapping components to URL
  • Routing behavior (e.g. navigation guards, redirects, ect.).
  • History API manipulation
  • Code splitting: fetching only the correct and minimal bundle, which the server has code-split. E.g. by filesystem based routes.
  • (Data loading)

To tie all of this into a client side "router".. results in a large API surface and often a larger bundle size (can you bundle split a router?).

Responsibilities which we typically DO NOT want to be handled by our router:

  • Animations / transitions. router5 rationale for this.
    • "React Router inevitably makes animations difficult, since it is designed to instantly route the current page position to its corresponding view-tree, with no regard for non-instantaneous animations. But ..." (2018 post and 2021 post)
  • Rendering: Co-dependency on rendering library. Ideally, we want routing to be decoupled from the rendering library we use (opposite to React Router).

xrouter is trying to minimize its scope by only handling serializing/deserializing application state to/from the URL and wrapping around the History API. The goal is for routing behavior to be handled by a library like xstate .

URL update should perhaps be a side-effect (not the primary API):

Who owns the URL? ... You must prevent and invalid URL from ever being reached otherwise the component mapped to that URL will be rendered. ... the URL is actually owned by the browser tab and your app’s state should determine what it should be, not the other way around.

But what if the user manually changes the URL in the browser tab? Should that, given client-side routing, immediately change the app state (not needing a page refresh)?

@redbar0n
Copy link
Author

redbar0n commented Sep 16, 2022

It seems wise to keep to these principles:

1. Routing as changes to app state

Separatable from the view / rendering. Inspired by router5.

Potentially modelled by a FSM or State Chart.

2. UI as a pure function of app state

"an application’s UI as a pure function of application state." -- G. Rauch https://rauchg.com/2015/pure-ui#f1

The seminal article UI as an afterthought by the MobX creator.

App state should not be a function or side-effect of rendering (not by useEffect nor by rendering <Route> components).

3. URL as the representation of app state

For shareability. Sharing a link should take another user to the app in the same state you were in.

Inspired by good old web standards, Solito.dev, and React Location aka. the upcoming TanStack Router.

Using the URL as the model for app state is also the most accessible mental model for web developers.

But only the client-state would be stored in the URL, so the server-state would still be downloaded and cached (and persisted/uploaded back to the server instantly). So receiving a URL from someone would be able to fully restore that same app state.

Also, the user would be able to make a change to the URL and see a change in app state (without a page refresh, or maybe the refresh could be hijacked beneficially…). Like in plain old stateless web pages, but this time as a single snapshot in time of a continuous app state.

Summary:

If you take into consideration these points, you have the chronology of user interaction:

Clicking a link to a new URL leads to a new app state, which then, through pure functions, renders the new UI.

As simple as that. In all, it's very similar to the old stateless page-model of the web, but in a manner where the app is stateful.

But what about when the user has multiple browser tabs on different pages? Where we potentially need to sync changes across tabs (read notifications etc.)? In this model, it would mean that the app is in multiple states at the same time...

Maybe sync some parts of the app state across tabs where desirable, but fundamentally treat each tab as the app being in a different state.

@redbar0n
Copy link
Author

redbar0n commented Sep 16, 2022

4. Derive URL from typed object, not from hardcoded string.

To eliminate dead links, and for improved DX due to type safety. So that TypeScript may warn the developer of broken links.

vercel/next.js#23439 (comment)

OR

Do the opposite: Create a typed object from parsing a typed string (since such URL parsing what the server would have to do anyway…).

By using one of the many typed routes libraries, the most intuitive/clear being:

typesafe-routes

I might come around to this position, if the string parsing is good enough, and simple enough in use even for parsing a URL that represent a complex app state.

Also, it would need to show type errors inside the hardcoded URL string, to catch typos and avoid broken URLs due to forgetfulness.

5. Derive XState action based on URL

Heck, even XState action naming schemes often mimic routes, like a‘SHOW.edit’ action…

If you use XState to handle routing, it might be a problem to connect actions/events with the URL. What if you also later forget to update either?

What you probably want is typesafe-routes and use the resulting route object to generate the correct action/event object for XState. So you can maintain the URL as the mental model, while also modelling routing as state changes in your FSM / State Chart.

@redbar0n
Copy link
Author

redbar0n commented Sep 18, 2022

Concerning filesystem routing, then in the context of React Native then expo-auto-navigation should be mentioned, with its new Expo routing API. It entered internal preview the week before Sept 17, 2022. It will share some concepts with NextJS, but not all, since some are a bit overcomplicated. But it should play nice with Solito.dev.

Update: expo-router is now public (still experimental). It appears like the successor to the expo-auto-navigation proof-of-concept.

@redbar0n
Copy link
Author

redbar0n commented Sep 22, 2022

In these examples the route state could be handled by a simple useState (at the root/top level) or similar (like Redux), that manipules a route object that represent the route state. Like in this small codesandbox example. XState actions could manipulate this route object using the setter setRoute.

Also, to avoid having

if (showWhen === null) return null;

inside every component. You could externalise the responsibility, and have:

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

which benefits the reader by bringing a bit more visual focus to the actual components instead of React Router's wrapping <Route> components.

For nested routes, if you don't want to list them all in one file like above, but distribute the routing responsibility, then you could pass the subroute into the component, and have it choose what to render. For example, the <Team> component would receive a subroute={currentRoute.teams} property, and do { <Team > && subroute.team.id }.

@redbar0n
Copy link
Author

To my original 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 ( components) that switches out the correct parts of the page?

I think we've found that we have to have if-conditions or && ternaries at some point in the render hierarchy, to tell React which components to render and which ones to hide. Because React won't automatically hide previously rendered components if their props doesn't change. In other words: React will continue to render components even when their props remain the same. So the premise in the question that React will "only render the parts of the view (component hierarchy) that changed" is not true. The VDOM diffing only updates the parts of the view that changed, but it still renders the other parts.

@redbar0n
Copy link
Author

redbar0n commented Sep 22, 2022

Relevant post on the evolution of the React Router API.

It also answers my side-question: "can you bundle split a (client-side single-file) router?":

However, the implementation we had in v3 made it difficult to do code-splitting since all your route components ended up in the same bundle (this was before React.lazy()). So as you added more routes, your bundle just kept growing.

@redbar0n
Copy link
Author

redbar0n commented Sep 22, 2022

A principle that is in favor of having:

if (showWhen === null) return null;

inside component is that components should be responsible for rendering themselves; the UI control should lie with the UI, not inside a router. That is the way of a decoupled architecture (each parts are responsible for themselves, and only listen to changes elsewhere and update themselves; not some parts directly controlling/manipulating other parts, but components observing route changes).

Animations/transitions

The benefit comes especially in dealing with animations/transitions.

Data loading

Another point is data-loading: We want to load data independently from UI rendering, due to view/state separation principle, but especially since fetching inside the UI component hierarchy gives the fetch waterfall problem.

By coupling the router to the view framework, like React Router, and if we also want to avoid the data fetch waterfall problem, the router becomes responsible for data loading (to be able to load data for components at several levels in the render hierarchy at once). This coupling is not necessarily what we want, as it becomes more of a framework than a library. Making later uncoupling/offboarding difficult.

Having the router handle data loading might impede native / cross-platform support:

React Native
You will use from React Native projects.
The data APIs from v6.4 are currently not supported in React Native, but should be eventually.

https://reactrouter.com/en/dev/routers/picking-a-router#react-native

@redbar0n
Copy link
Author

redbar0n commented Sep 22, 2022

Interestingly the prototype for the React Router API looked like this:

<App>
  <Home path={"/"} />
  <Teams path={"/teams"}>
    <Team path={"/teams/team/:id"}>
      <EditTeam path={"/teams/team/edit"} />
      <NewTeamForm path={"/teams/team/new"} />
    </Team>
  </Teams>
</App>
<PageLayout>
  <Privacy path={"/privacy"} />
  <ToS path={"/tos"} />
</PageLayout>
<Contact path={"/contact_us"} />

Which is very similar to my initial proposal (and also similar to React Router v4 according to Ryan Florence in that presentation). The reason they moved away from that API in React Router v1, v2 and v3 was because:

  • when server-rendering React, people were asking for "How can I know which components will render before I render them?", to know which data to load.

Why did they want that?

Likely: Because each component has their own data dependencies/requirements. If they got the data with fetch inside components while rendering (which would be reasonable with nested routes on the client side if React Router would only swap out a part of the page), they'd have the fetch waterfall problem when trying to render an entire page. (A problem that using Relay with GraphQL fragments would have solved, but people were probably using React with familiar fetch patterns).

On the server, the URL determined:

  • What components to render.
  • What data to render in those components.

But React decides which components to render. So the React Router team decided to move the components outside of React (in the React Router v1, v2 and v3 API). So that React Router could strip their props and statically match URL with routes, and return a list (to the developer) of the routes that matched. But that move led to the router having taken over the responsibility of rendering from React, which turned out to be a mistake, as he explains. They had basically made a combined Router and Controller, which decided what to render when. Taking in a whole route config object (or listing out all the routes at the top level). This is an API they've actually returned to in v6 with the exception that it "allows you to spread routes across your entire app instead of defining them all up front as a prop to as we did in v3".

@redbar0n
Copy link
Author

redbar0n commented Sep 22, 2022

React Router v6 with a route object now looks like this:

// src/main.jsx
createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    loader: rootLoader,
    action: rootAction,
    errorElement: <ErrorPage />,
    children: [
      {
        errorElement: <ErrorPage />,
        children: [
          { index: true, element: <Index /> },
          {
            path: "contacts/:contactId",
            element: <Contact />,
            loader: contactLoader,
            action: contactAction,
          },
          /* the rest of the routes */
        ],
      },
    ],
  },
]);

Here is an extended example.

Which looks a lot like the API of React Location / TanStack Router!

React Router has similarities to XState

Notice first that the loaders (i.e. readers/queries) and actions (i.e. writers/mutations) of React Router are similar to XState actions, but most likely less powerful.

Beside, how does React Router v6 avoid the onChange, onEnter and onLeave hooks they previously branded as a mistake?
(Which btw look similar to XState entry and exit actions.) Thinking especially of transitions/animations.

Although the following seems like a feature React Router has that XState via its routers xstate-router or xrouter doesn't have:

As the user navigates around the app, the loaders for the next matching branch of routes will be called in parallel ...

React Router duplicates React's component tree

Notice that "the loader + action combo is similar to the Controller in MVC" as viktorsavkin said. This relates to the "Sometimes MVC is applied at the individual widget level ..." paragraph in this MVC post, which highlights a problem: "which of the three [Model, View or Controller] has custody of the children"? In React Router - that places the custody with its route object (which is effectively a controller) - the component tree custody is duplicated: once in React and once in React Router... Because React Router directly drives React.

This would also be a problem if you use XState to directly drive the route changes, from a centralized place:

if you model the route state as a state machine router, you'd have to define the component hierarchy twice: once in the render tree, and once in the state tree (connecting all the machines). That's cumbersome, and a bit of boilerplate. Not to mention trying to keep them in sync, over time... Or trying to code-split this router. me on other issue

But in my MVC-widget experiment, I attempt to reuse the component tree (since I don't think XState needs to directly drive rendering in React, but merely change a route state that React picks up). This would afford decoupling the routing changes from a centralized "router", into a more "decentralized routing" solution. This could potentially be taken even further, inspired by the quote:

Don't communicate by sharing memory; share memory by communicating. (R. Pike)

So maybe changing routes should be done by components passing messages to each other (in line with the actor model, which is used in XState), instead of modifying a shared route state/object? But since multiple components need to be affected by a route change, you couldn't send an action/event to a single component (unless you have a centralized router). So you would need some kind of actor broadcasting.

So maybe "a router" or "a Controller" is the wrong focus to have, and we should seek for "routing" as something decentralized. It would be better for code-splitting too, I would imagine.

@redbar0n
Copy link
Author

redbar0n commented Sep 23, 2022

To my two previous questions:

URL update should perhaps be a side-effect (not the primary API):

Who owns the URL? ... You must prevent and invalid URL from ever being reached otherwise the component mapped to that URL will be rendered. ... the URL is actually owned by the browser tab and your app’s state should determine what it should be, not the other way around.

But what if the user manually changes the URL in the browser tab? Should that, given client-side routing, immediately change the app state (not needing a page refresh)?

In How to decouple state and UI, Michel Weststrate answers:

Whenever a URL is entered in the browser’s address bar the store will transition to the correct state and the correct UI is rendered. However, the inverse process is missing. If we click a document in the overview, the URL in the address bar should be updated.

One could simply fix this by calling history.pushState in the appropriate actions of the store. That would work, but hold your horses. That approach has two downsides. First, it would make our store browser aware. Secondly, it is a tedious, imperative approach to the problem. If your UI has many possible views, you would end up with pushState calls in a lot of places.

Consider this: the URL of the application is just a representation of the state of our application. Like the UI, it can be derived completely from the current application state.

The state store ought not to be browser aware, since it could be run in different environments, like on a server, without the browser history API.

@redbar0n
Copy link
Author

Useful router5 descriptions:

router5
This is a more extensive library for routing. It's unique feature is that routes are organized as a tree, made of segments and nodes. It aims to be framework agnostic and uses middleware and plugins to adapt to different frameworks. Of course, with flexibility comes complexity. mobx-state-router makes some choices for you to keep the overall learning curve simple. -- From https://react.libhunt.com/router5-alternatives

Router5 is an alternative for React-Router, with support for async API calls, transition middleware and excellent URL integration. Parameter support, fallback routes and route definitions offer the flexibility and stability you were looking for. Platform agnostic but works well with React and optionally MobX. -- From https://github.com/nareshbhatia/mobx-state-router

@redbar0n
Copy link
Author

redbar0n commented Sep 28, 2022

The new Expo Router, for crossplatform routing: https://github.com/expo/router/

It uses react-navigation internally. (But RN’s Metro bundler has poor web support, so it might be difficult to use this with RNW?)

@redbar0n
Copy link
Author

for crossplatform routing, see also

react-native-url-router which relies on React Router and replaces react-navigation on native.

@redbar0n
Copy link
Author

redbar0n commented Sep 28, 2022

@redbar0n
Copy link
Author

To my question / critique of React Router:

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 ( components) that switches out the correct parts of the page?

I just came across this text, which describes some of the same sentiment

It's enough of a problem that React Router has redesigned its API for the 6th time, with an explicit solution. Now a can contain an , which is an explicit slot to be filled with dynamic page contents. You can also nest layouts and route definitions more easily, letting the Router do the work of wrapping.

It's useful, but to me, this feels kinda backwards. An serves the same purpose as an ordinary React children prop. This pattern is reinventing something that already exists, just to enable different semantics. And there is only one outlet per route. There is a simpler alternative: what if React could just keep all the children when it remounts a parent?

https://acko.net/blog/react-the-missing-parts/

But instead of the solution he then suggests (a component that morphs), I suggest, in the context of an SPA, to not represent the route as a top level component at all. But simply broadcast a message to all components/actors about the route change, to which they respond by rendering or not rendering themselves. The existing sidebar, header and other components that are part of a permanent layout should by not re-render themselves, if they don't receive a prop/message that indicates a change. Thus, they will be automatically shared across routes.

A benefit of this approach is that you don't restrict the rendering of components to a simple hierarchy, and you don't need to have a single for a given layout. But won't it be chaotic, and hard to track down bugs? Not if the components/actors are modeled with a tool such as XState, that has full control over all state changes (makes disallowed state changes unreachable), and can even visualize and show a step-through of the allowable interactions.

@redbar0n
Copy link
Author

React Router routes looks like this (TanStack router is similar):

// src/main.jsx
createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    loader: rootLoader,
    action: rootAction,
    errorElement: <ErrorPage />,
    children: [
      {
        errorElement: <ErrorPage />,
        children: [
          { index: true, element: <Index /> },
          {
            path: "contacts/:contactId",
            element: <Contact />,
            loader: contactLoader,
            action: contactAction,
          },
          /* the rest of the routes */
        ],
      },
    ],
  },
]);

But it's annoying with the incessant nesting, writing path and children and everything. I want to be able to quickly glance at a file and just visually pattern-match the route to the URL which I am seeing in the browser. So what about instead of a nested tree, which you have to trace and parse, instead have flat routes (with a little bit of duplication in the path names)? Something like this:

// format: [path, element, loader, action, errorElement]
const routes  = [
  ['/', <Root />, rootLoader, rootAction, <ErrorPage />],
  ['/contacts/:contactId', <Contact />, contactLoader, contactAction, null]
]

Or, maybe you could even infer the loaders and actions based on Convetion over Configuration, so that they always have a default name (derived from the element name) unless overridden? Same with a default ErrorPage.

Trimming it down to:

// format: [path, element, loader?, action?, errorElement?]
const routes  = [
  ['/', <Root />],
  ['/contacts/:contactId', <Contact />]
]

@redbar0n
Copy link
Author

redbar0n commented Oct 31, 2022

xstate-tree has an interesting solution for routing:

Since xstate-tree is designed around a hierarchical tree of machines, routing can't function similar to how react-router works

Instead, routing is based around the construction of route objects representing specific urls. Route objects can be composed together to create hierarchies, designed to mimic the xstate-tree machine hierarchy, but not required to match the actual hierarchy of machines

It seems that you could have a URL lead to updating multiple separate sub-component hierarchies in the UI. Unlike React Router, which has a tight mapping from the URL to a single UI component hierarchy (only one "russian doll"). This single hierarchy approach of React Router is perhaps best illustrated by React Router's sister project Remix (see the example.com/sales/invoices/102000 animation there):

Websites usually have levels of navigation that control child views.
Not only are these components pretty much always coupled to URL segments...
...they’re also the semantic boundary of data loading and code splitting.

@redbar0n
Copy link
Author

redbar0n commented Dec 7, 2022

UI Router could be an inspiration (for setting up equivalent structure with XState), as UI Router is state machine based.

@redbar0n
Copy link
Author

TanStack Router source code.

@redbar0n
Copy link
Author

My Twitter discussion with Chris Shank of XRouter: https://twitter.com/magnemg/status/1565019599778856960

@redbar0n
Copy link
Author

redbar0n commented Jan 5, 2024

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.

What is a router, really?

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

@redbar0n
Copy link
Author

redbar0n commented Jan 7, 2024

NextJS App Router rationale:

"Every part of the framework has to be designed around the router.

  • page transitions,
  • data fetching,
  • caching,
  • mutating and revalidating data,
  • streaming,
  • styling content,
  • and more.

To make our router compatible with streaming, and to solve these requests for enhanced support for layouts, we set out to build a new version of our router."
https://nextjs.org/blog/next-13-4

@redbar0n
Copy link
Author

redbar0n commented Jan 8, 2024

What if a Router is the wrong abstraction? What if it is like creating something akin to a UniversalController?

Maybe routing is so ingrained with the app architecture that the user should be in charge of it, and compose the routing from a set of primitives or independent libraries?


I remember Rails had a separate routes file aka. "router/dispatcher", but all route handling logic went into individual Controllers for each of the Models. Not a single UniversalController / Router where everything was centralized.

The Rails router recognizes URLs and dispatches them to a controller's action, or to a Rack application. It can also generate paths and URLs, avoiding the need to hardcode strings in your views. -- rails routing

@ChrisShank
Copy link

ChrisShank commented Jan 9, 2024

Maybe routing is so ingrained with the app architecture that the user should be in charge of it, and compose the routing from a set of primitives or independent libraries?

You have reinvigorated my interest here and I definitely agree with the sentiment. I think I have some opinion on what those boundaries for routing primitives should be... publishing them to npm soon  🙂

https://github.com/ChrisShank/routtl

Aside: Sorts of concerns me that "cutting-edge routers" have such a large scope and require a CLI to achieve type-safety 😅

image

@redbar0n
Copy link
Author

@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