remix-run / react-router

Declarative routing for React
https://reactrouter.com
MIT License
52.98k stars 10.29k forks source link

Experiment with middleware-based, functional routing #2726

Closed acdlite closed 8 years ago

acdlite commented 8 years ago

Hi gang,

Not sure the correct venue for this discussion, but I've been experimenting lately with a middleware-based solution for client side routing. Based partly on Redux middleware, but also on how middleware works in server-side applications as well.

I have an initial version of the experiment up at acdlite/router.

My goal is not to replace React Router, but to brainstorm some ways to improve its internal architecture, make it more flexible, and solve some frustrating edge cases I've personally encountered during my time using it.

I'm using it in a side project right now. Hopefully I'll learn some things and we can use them to improve React Router. Or maybe I'll learn nothing and it'll be a waste of time :) Either way, would love to receive feedback if/when you have the time.

@rackt/routing

timdorr commented 8 years ago

@acdlite Have you been following along with the v2 discussion? In particular, the removal of useRoutes should be of most interest. It looks like a similar goal, just with a differing implementation.

ryanflorence commented 8 years ago

Have you read the 2.0 (1.1?) roadmap?

There's two types of middleware in React Router 1.1 (2.0?)

  1. "history" middleware that's functional
  2. "rendering" middleware that requires the lifecycle hooks of components

I'm really happy with the balance we're finding in the next release with both types of middleware. From what I can tell, what you've got only replaces (1), which on the surface looks like bikeshedding history's composability API? I haven't given it a close enough look though.

I'm going to close this though to keep us on track for the next release (we all get tempted with re-architecturing) but please keep exploring, maybe you'll find something we should be doing.

taion commented 8 years ago

This touches on a conversation that @timdorr, @mjackson, and I had on Discord.

The thorn is that server-side rendering in React Router requires creating a history object, but this history object is never used to do anything really interesting and history-related. Given that, you can really break down this API into 3 separate domains:

  1. Location mapping; converts location descriptors to locations, and generates paths and hrefs (for links)
  2. History management; navigating back/forward, running transition hooks, &c.
  3. Route resolution/rendering from location

Currently we bundle (1) and (2) under history and put (3) in React Router. This is suboptimal, because it means that for server-side rendering, we pull in (2) for no reason - we only need (1) and (3) to render on the server.

Under this taxonomy, I think acdlite/router project actually covers (1) and (3), so it's a bit of a different way to split things up.

I do think longer term we want to separate out the bag of utilities in (1) outside of history - perhaps with a different compositional paradigm as well (maybe). Just have some location resolution object that can handle things like queries, basenames, named paths, &c.. Then have (2) has a pure history layer. (3) I think can be close to what it is now? Not so sure.

One wrinkle here is that implementing the "confirming navigation" pattern actually requires the router to be aware that transitions are a thing that exist, though. I'm not really sure how they'd work in the acdlite/router paradigm, since they require knowing about both routes and transitions.

ryanflorence commented 8 years ago

(3) can actually be split into two as well with (3) location mapping to routes--match--and (4) rendering the matched routes.

A while back, before the 1.0 rewrite, I made this thing that does (3) https://github.com/ryanflorence/nested-router, I suspect when @mjackson starts/finishes the useRoutes refactor we'll end up with (3) inside of Router as match that's primed and ready to be moved out as well!

So yeah ... lots of ways to do this, I'm just happy more people than @mjackson and I are thinking about it :)

taion commented 8 years ago

Well, useRoutes sort of already gives you that - just the route matching without the rendering.

In some sense, we're already offering this sort of customizability via <Router RoutingContext>, and will continue to do so via <Router render>. If you think about it this way, <Router> is just a bit of glue between useRoutes/match and <RoutingContext>, which is a nice way to set things up.

acdlite commented 8 years ago

I am (somewhat) familiar with the upcoming API for history middleware; I've come to believe that coupling of history to routing logic isn't the right approach.

acdlite/router is focused exclusively on location => state, with the idea that history navigation can be totally separate. If true (and thus far, my experiment seems to indicate it is) it makes the server-side problem go away.

taion commented 8 years ago

There is no change in API for history middleware - the idea is to stop pretending that useRoutes is meaningful as a history middleware/enhancer and just have the matching be its own thing.

We do still want to separate out location descriptor -> location from history management to get cleaner SSR, like you stated, but that's orthogonal and further down the road.

ryanflorence commented 8 years ago

coupling of history to routing logic

Yeah, that's exactly what the next API is about, decoupling them.

ryanflorence commented 8 years ago

it makes the server-side problem go away.

What problem? We used to have Router.run which had the same API on client and server.

taion commented 8 years ago

I'm just using that as shorthand for "it's a little messy to have to create a history to do server-side routing".

For server side routing we need to convert the path/location descriptor for a location object, then create hrefs for <Link>s. We don't need something with a push/replace method.

@mjackson sounded like he was pretty keen on eventually getting rid of it, which I think would require separating out the location-related functionality from the history-related functionality.

acdlite commented 8 years ago

"it's a little messy to have to create a history to do server-side routing".

That's kind of what I'm getting at.

taion commented 8 years ago

Yup, so just to be clear, I think this is going to be a nice cleanup, but I don't know that it'd change the API to the router dramatically - e.g. match mostly hides the history from you, and we're moving to putting all the methods on a router object on context anyway, so having e.g. locationer.createHref v history.push... I mean, at least the way I'm looking at it, not a big difference from the PoV of users.

ryanflorence commented 8 years ago

Users don't have to create a history on the server, they just hand a url to match.

Histories listen to the url and create paths. You could have a "history" and a "path creator" if seeing the word "history" inside of match, away from users, bothers you that much.

Note that they would always come in pairs, and I'm not sure anybody using a router would be excited about passing two objects instead of one to their router because the maintainers sleep better at night knowing they've drawn every line perfectly in the code :P

Perhaps history is the wrong word and it should have been something like "location manager" or whatever. Regardless, the only people who deal with a "false history" on the server are the maintainers of match!

acdlite commented 8 years ago

I agree from a typical user perspective, this has a much-ado-about-nothing feeling to it.

I'm coming from a slightly different perspective, as a "power user" who wants as much control/flexibility with routing and React Router as I do with state management and Redux.

I have a lot of thoughts about this... don't have time now, but I'll try to collect my thoughts and present something more cogent soon.

taion commented 8 years ago

I totally agree with you - I'm just going off the discussion on https://github.com/rackt/react-router/pull/2680#issuecomment-163064194.

I don't think this matters at all to the majority of our users. Like you said, it just might be a nice architectural cleanup for our benefit at some point down the road.

But it seemed important enough that we rejected #2680, and are keeping around the extra history.createLocation API point for now, which is a cost in terms of API surface area.

ryanflorence commented 8 years ago

I'm coming from a slightly different perspective, as a "power user" who wants as much control/flexibility with routing and React Router as I do with state management and Redux.

Cool. Maybe I've lost my creativity but can you give a few use-cases? Cause the current history wrapping + the new render prop cover everything I can dream of for a power user. Is there a use-case those don't solve? Or is there something awkward about their APIs?

ryanflorence commented 8 years ago

Also, sorry if I'm coming across grumpy, I'm simultaneously trying to get some new health insurance and these new government websites are as bad as the insurance companies.

screen shot 2015-12-15 at 5 36 08 pm

Clicking the question mark to figure out if a checked box means yes or no actually checks the box!

taion commented 8 years ago

Let's suppose we want first-order named routes support. (STRAWMAN!)

We can code this up as a history enhancer. Call it useNamedPaths. We can spin one up as useNamedPaths(useQueries(createHistory))({ routes }), and use the new push and replace hooks to let the user do history.push({ name: "foo", params: { fooId: 3 } }), and maybe this even works out-of-the-box with <Link>.

Now we want to do server-side rendering. At this point the clean match API breaks down a bit - the user has to create a history object himself or herself. Feels a bit arbitrary and weird, and maybe you'd be hackishly inclined to do something "incorrect" in your useNamedRoutes history enhancer, whereas it'd be clearer to you what the right hooks were if we made it clearly only do location management.

Weird and contrived example, but first one off the top of my head.

ryanflorence commented 8 years ago

I think this is where we pass in a history and a matcher to Router. We've already learned not to mix matching routes w/ the history objects.

(named routes is a perfect example actually, because it requires entry points into every step of the routing process)

timdorr commented 8 years ago

I think this is just an audience issue more than anything. React-router targets the "common person" approach and reduces boilerplate for the 90% use case. @acdlite's router sacrifices boilerplate reduction at the affordance of greater flexibility and control, something essential to power users. One will be good at things the other is bad at, and vice versa. But I don't think either approach makes anything impossible, just better-suited for different audiences.

I don't think there's really a middle ground, as they are fundamentally different in approach and implementation. But underlying what I'm seeing shows a similar goal. The difference being who that goat's solution is being built for.

ryanflorence commented 8 years ago

A couple of things to help you out @acdlite

  1. We really like the render(<Router .../>) API and will probably never sacrifice it for any amount of awesome advanced user stuff. Our approach is a dual API: advanced users use match and RouterContext for cool stuff.
  2. Link will kill you, don't underestimate the complexity it adds (and I suppose if you can get rid of the complexity, :+1:)
  3. "leave hooks" on routes, but especially in components, will kill you even more.
  4. To really get our attention (and our time/thoughts/help), bring us use-cases, not "engineer marketing"

Can't wait to see what you come up with, no doubt it'll be informative to future versions :D

taion commented 8 years ago

@ryanflorence Named path support actually can be done just by enhancing the "location manager" layer. You just need to support location descriptors that specify names and params. Everything else should work off the generated locations, which will have pathnames and whatever, and won't have to care about the named routes at all. Named "route" support is just named path support + the ability to understand your route definitions - but it's not a two-way link.

@timdorr I don't believe in that dichotomy. I think it's often possible to have a fantastic, modular API internally while still exposing nice monolithic entry points to users, then allow users to peel the onion as they need more. For example, the API entry point to react-router-relay is just using <RelayRouter> instead of a standard <Router> - the user doesn't have to think about how we customize routing contexts at all.

ryanflorence commented 8 years ago

fantastic, modular API internally while still exposing nice monolithic entry points to users, then allow users to peel the onion as they need more.

yes, this is exactly what we've been trying to do with history, locations, RouterContext and match.

taion commented 8 years ago

:+1: I think we're moving in all the right directions there.

One more note - while supporting power users and making our lives easier with better APIs is a plus, we also shouldn't underestimate the pedagogical benefits of having a well-structured code base with good abstractions.

I don't think there's any better way to encourage people to learn code other than by showing them good code, but at a micro level and at a broad architectural level. React Router is in a pretty special position of being a relatively "small" library that is still of a ton of utility - it's a very good place for people to practice reading code to understand how code works, and that's a constituency that's quite important to me.

Being able to dig into source is an important skill I want my devs to have - to get a sense of what other developers do and to be able to really understand code by reading it. You can't really do that with something like React or Relay or whatever because they're too complicated. Smaller projects like React Router or Redux are great for that, though, and that's a prime reason to keep code quality higher even than it would need to be otherwise.

ryanflorence commented 8 years ago

also shouldn't underestimate the pedagogical benefits of having a well-structured code base with good abstractions

Yeah, pretty sure I'm the only one who pushes the other direction :P I agree with what you say, I learned JS (and programming) by reading MooTools' source.