bikeshaving / shovel

Dig for treasure 🚧 Under Construction 🚧
MIT License
5 stars 0 forks source link

Router #4

Open brainkim opened 1 year ago

brainkim commented 1 year ago

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:

Things I don’t like:

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.

Things I don’t like

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:

I don’t like:

CLEARLY, I am ambivalent about nested routing patterns.

brainkim commented 1 year ago

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>
  2. 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>
  3. 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>
  4. 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 commented 1 year ago

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:

brainkim commented 1 year ago

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 commented 8 months ago

Okay. Some new-ish thoughts about routing

My hate for file-system routing persists

Why? So many reasons!

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?