Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Router #4

Open
brainkim opened this issue Apr 2, 2023 · 4 comments
Open

Router #4

brainkim opened this issue Apr 2, 2023 · 4 comments

Comments

@brainkim
Copy link
Member

brainkim commented Apr 2, 2023

See #3 for context about the broader project.

I want a router. Here are some popular styles for routers out there. We will not be considering file-system based routing options.

Express.js

const express = require('express');
const app = express();

const router = express.Router();

router.get('/:id', (req, res) => {
  res.send(`This is the page for item ${req.params.id}`);
});

router.post('/', (req, res) => {
  // do something with the POST data
  res.send('POST request received');
});

app.use('/items', router);

Things I like:

  • Callback code is in the second-level scope. Fewer indents good.
  • path-to-regexp
  • Callback is the same style as the middleware system (minus the next()).

Things I don’t like:

  • HTTP verbs as methods is whack. If you’re RESTful or whatever, you gotta duplicate path definitions. Also I don’t like using get() for responses which do HTML. That feels like it should be separate.
  • FETCH PLEASE

Vue Router

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from './components/Home.vue'
import About from './components/About.vue'

Vue.use(VueRouter)

const router = new VueRouter({
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About }
  ]
})

Vue Router seems to inject params and other routing data with a special $route. Child routing, with automatic component nesting. Could be nice but again is pretty framework specific.

const routes = [
  {
    path: '/user/:id',
    component: User,
    children: [
      {
        // UserProfile will be rendered inside User's <router-view>
        // when /user/:id/profile is matched
        path: 'profile',
        component: UserProfile,
      },
      {
        // UserPosts will be rendered inside User's <router-view>
        // when /user/:id/posts is matched
        path: 'posts',
        component: UserPosts,
      },
    ],
  },
]

Things I like.

  • Data representation of routes with names.
  • Component-level integration.
  • Components can nest.
  • Client-side routing.

Things I don’t like

  • FETCH PLEASE
  • do I want to make the router frontend framework agnostic?
  • Components can nest.

Django

from django.urls import path, re_path
from django.views.generic import TemplateView
from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('about/', views.about, name='about'),
    path('contact/', views.contact, name='contact'),
    path('blog/', views.blog_list, name='blog_list'),
    path('blog/<int:year>/<int:month>/<slug:slug>/', views.blog_post, name='blog_post'),
    path('products/', views.product_list, name='product_list'),
    path('products/<slug:category>/', views.product_list, name='product_list_category'),
    path('products/<slug:category>/<slug:product>/', views.product_detail, name='product_detail'),
    re_path(r'^search/(?P<query>\w+)/$', views.search, name='search'),
    path('signup/', views.signup, name='signup'),
    path('login/', views.login, name='login'),
    path('logout/', views.logout, name='logout'),
    path('profile/', views.profile, name='profile'),
    path('profile/edit/', views.edit_profile, name='edit_profile'),
    path('password/change/', views.change_password, name='change_password'),
    path('terms/', TemplateView.as_view(template_name='terms.html'), name='terms'),
    path('privacy/', TemplateView.as_view(template_name='privacy.html'), name='privacy'),
]

Here’s the nesting concept:

from django.urls import path, include
from . import views

urlpatterns = [
    path('accounts/', include('accounts.urls')),
    path('blog/', include('blog.urls')),
    path('shop/', include('shop.urls')),
    path('', views.index, name='index'),
]

Where it all started for me. Honestly, I just want to port this.

I like:

  • Explicit fn call. Prioritizes the path.
  • Linear path matching. No fancy route selection method. Deterministic. Easy to preserve legacy routes.
  • The view type is flexible. There’s a function and class based view abstraction which lets you define html templates and stuff. You can import url patterns from other “apps” or “libraries” to implement RESTful APIs.
  • Labeled matches and path converters. Maybe path-to-regexp has an alternative.
  • The views don’t nest. Nesting is done by templates.

I don’t like:

  • Too many view classes. Probably could reduce it to a function or an interface.
  • No component knowledge. The nested urls include() is for managing a lot of paths for a larger application.
  • The views don’t nest. The nesting patterns are done via templates.

CLEARLY, I am ambivalent about nested routing patterns.

@brainkim
Copy link
Member Author

brainkim commented Apr 3, 2023

Here’s React Router. Had to do some research with ChatGPT because there are too many breaking changes across the years. I like using JSX children. Most development after React Router v4 seems to be about working around React limitations and chasing after hooks insanity.

ChatGPT:

  1. React Router v1 (October 2015):
<Router>
  <Route path="/" component={Home} />
  <Route path="/about" component={About}>
    <Route path="team" component={Team} />
  </Route>
  <Route path="/contact" component={Contact} />
</Router>
  1. React Router v2 (February 2016) & v3 (November 2016):
<Router>
  <Route path="/" component={Home} />
  <Route path="/about" component={About}>
    <Route path="team" component={Team} />
  </Route>
  <Route path="/contact" component={Contact} />
</Router>
  1. React Router v4 (March 2017) & v5 (May 2019) - Using JSX children:
<BrowserRouter>
  <div>
    <Route exact path="/">
      <Home />
    </Route>
    <Route path="/about">
      <About>
        <Route path="/about/team">
          <Team />
        </Route>
      </About>
    </Route>
    <Route path="/contact">
      <Contact />
    </Route>
  </div>
</BrowserRouter>
  1. React Router v4 (March 2017) & v5 (May 2019) - Using Switch:
<BrowserRouter>
  <Switch>
    <Route exact path="/" component={Home} />
    <Route path="/about" component={About} />
    <Route path="/contact" component={Contact} />
  </Switch>
</BrowserRouter>
  1. React Router v6 (November 2021):
<BrowserRouter>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
    <Route path="/contact" element={<Contact />} />
  </Routes>
</BrowserRouter>

I sincerely do not understand the marginal differences between children, component and element and have no idea why React Router would subject users to such migrations. Again, I really only like using the nesting concept which uses children, though there would probably still have to be some kind of <Outlet /> concept.

Edit: <Outlet /> only makes sense when you have a component or element style approach. If you use children props, <Outlet /> is unnecessary.

@brainkim
Copy link
Member Author

brainkim commented Apr 6, 2023

Okay. Here are my latest cogitations on a router API.
For the Crank website, it might like this:

const routes = <>
  <Route path="/">
    <HomeView />
  </Route>
  <Route path="/blog">
    <BlogHomeView />
  </Route>
  <Route path="/blog/:slug">
    <BlogView />
  </Route>
  <Route path="/guides/:slug">
    <GuideView />
  </Route>
  <Route path="/playground">
    <PlaygroundView />
  </Route>
</>;

With the jsx template tag:

const routes = jsx`
  <${Route} path="/">
    <${HomeView} />
  <//Route>
  <${Route} path="/blog">
    <${BlogHomeView} />
  <//Route>
  <${Route} path="/blog/:slug">
    <${BlogView} />
  <//Route>
  <${Route} path="/guides/:slug">
    <${GuideView} />
  <//Route>
  <${Route} path="/playground">
    <${PlaygroundView} />
  <//Route>
`;

The real value of routes as JSX is when you have nested routes.

const routes = <>
  <Route path="/">
    <HomeView />
  </Route>
  <Route path="/blog">
    <BlogNavbar />
    <BlogSidebar />
    <Route path="">
      <BlogHomeView />
    </Route>
    <Route path="/:slug">
      <BlogView />
    </Route>
  </Route>
</>;

Routes can be nested arbitrarily. Should probably have Path.join() semantics, or really, new URL(source, dest) semantics.

Some things I’ve been thinking about:

  • Avoid exact prop. The Route with the empty path is the preferable way to allow for home views versus detail views because it doesn’t change the semantics of the path matching. Should we allow for multiple potential matches for routes and what does that actually look like? How is it implemented?
  • Not Found routes. When routes aren’t found, do we want to handle that somewhere else? Can we render with statusCodes perhaps? Should we be using components to display 404 information?

@brainkim
Copy link
Member Author

brainkim commented Apr 7, 2023

Some more thoughts, less coherent:

One thing I don’t like about context-style APIs, is that I often want to pull the data directly from the Route, somehow, like when I see:

<Route path="/:slug">
  <BlogView />
</Route>

I want to be able to inline the <BlogView /> component into the Route somehow, but I can’t, because the separate component abstraction is needed to extract the path matching information. If I were just using a regular API call, this wouldn’t be a problem, but we have chosen to design our APIs around JSX.

This is a struggle I have when working with Provider-style APIs in general, not really specific to .

Another thing, how do routes actually work with the whole HTML thing? One feature I really want with shovel is to put the rendering of the entire HTML page in user-space. In other words, there shouldn’t be some hidden abstraction for the layout of pages, it should just be identifiable from the rendering of the response, jump to definition, everything from <!DOCTYPE html> to </body> should be discoverable. The problem is that there are two things you want to put in the route body in a typical app: some metadata in the head, and a rendering of the app in the body.

There are three solutions:

Option 1:

<Route path=":slug">
  <ComponentIncludingHeadandBody />
</Route>

Put the entire document in the route children. This works, and is accurate but subjectively feels dumb. It reduces the flexibility of putting routes in JSX, and if this is the only solution then maybe there’s something to be said about not going down the routes in JSX path. Path information can be extracted with providers/consumers, but is that all we get?

Option 2:

<head>
  <HelmetMeta />
</head>
<Route path=":slug">
  {/* In some component or code */}
  <Helmet title={title} />
</Route>

The react-helmet solution. I’m not really a big fan of this sorta abstraction. Kinda confusing to have route-dependent rendering outside of a route, debugging heads is kinda hard because you have to search the entire application tree, and it’s a pain in the ass to implement, likely you’ll need a double render.

Option 3:
Multiple Route paths?????

<head>
  {/* head stuff */}
  <Route path="blog">
    <Route path="/:slug">
      <BlogHead />
    </Route>
  </Route>
</head>
<body>
  <Route path="blog">
    <BlogView />
  </Route>
</body>

ME NO LIKE TO DEFINE ROUTE TREE TWICE.


Honestly, some version of Option 1 will likely have to suffice.

I guess I am still struggling with why routing needs to go into JSX. The apps nested in paths and dashboards and stuff is cool but I am sweating the details now.

@brainkim
Copy link
Member Author

brainkim commented Dec 31, 2023

Okay. Some new-ish thoughts about routing

My hate for file-system routing persists

Why? So many reasons!

  • Filenames as syntax sucks. The syntax is exotic because of limitations to what characters you can put in a filename (blog/[id].js, routes/[...catchall]/blah.js). Many valid URL characters are invalid, especially if you support Windows.
  • Directories of files are not as expressive as modules. File system based routing usually applies some conventions to what file in a directory does what, but this means that you end up opening dozens of files with the same name. An average SvelteKit development session might involve opening a dozen or so +page.svelte and +layout.svelte files.
  • Directories of routes are not linear, so the routing has to deal with heuristics like most to least specific route.
  • Directories of route files are tough to reason about. You spend your time expanding/collapsing nested directories, reading about the special file names and ways to implement layout nesting. Because of the priority heuristics, deleting routes can be unpredictable.
  • Directories aren’t version-able. As far as I know, you can’t blame a directory in git for removed files. And added or changed paths are similarly difficult to surface. Having all the routing code in a single file lets you use source control the way it is meant to be used.
  • File-based routing systems require introspection of source code. This almost guarantees extra build time work, because a lot of serverless platforms don’t let you introspect a directory of source files for no reason. This also makes it harder to access routing data or serialize in the case of trying to pass the data along to smart clients or static edge routing situations.

In short, I think that it’s essential for routing abstractions to be obvious and readable, and putting this crucial data in the filesystem is both a rough developer experience.

React Router style routing

The same sorts of objections hold with putting route configuration in JSX element trees. Nesting and reuse of layouts can be done with code reuse in the various handlers, non-linear matching is again a problem, and the route definitions are hard to read because of their nesting and all the noise.

URLPattern is a thing!

I’ve seen this before but a Netlify blog post reminded me about the upcoming URLPattern class. https://developer.mozilla.org/en-US/docs/Web/API/URLPattern. Anything “standard-aligned” is something I want to adopt immediately, and the API seems to cover a lot of use-cases.

Isomorphism

What I really want from the routing abstraction is something which can be defined once and used on both client and server. I initially thought that the way to achieve this would be to have the user define routes in a way which can be imported both on the client and server, but I don’t like this level of indirection. Route configurations should directly reference server handlers so you can jump to definition and do analysis. I think that the best way to share route configurations on the server and client is the serialize the routes on the server and send them down to the client to be used there somehow.

Steps forward

Probably going to try to write a router which looks most like the Django style list of linear paths, with URLPattern-based syntax. I’m still not sure what that looks like, like is it an array whose members are calls to a bunch of route() functions, or a class instantiation, or maybe a router should be injected into the server entry point somehow? I do want to implement the includes-like behavior, where you can nested routes under a subpath, but I dunno.

I also wonder if we can add redirects, rewrites and proxies to this configuration API?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant