solidjs / solid

A declarative, efficient, and flexible JavaScript library for building user interfaces.
https://solidjs.com
MIT License
32.17k stars 918 forks source link

The Solid Router #186

Closed aleksanderd closed 3 years ago

aleksanderd commented 4 years ago

Hi!

First, I want say huge thanks to SOLID authors! I tried vue,react,angular,svelte,stenciljs and SOLID seems to be inspiring hope in this world ;)

But there is not too many mature libs around it yet, for example router...

I found several:
https://github.com/mduclehcm/solid-router from @mduclehcm https://github.com/rturnq/solid-router from @rturnq https://github.com/Seanmclem/solidjs-router from @Seanmclem

After some issues and researching code, I decided to play with some ideas from the scratch just for learning and better understanding the SOLID features. So... there is one more router for now :)

https://github.com/zxlib/solid-router

I'll be grateful for any code reviews, comments, etc.

But the issue is about The Solid Router in general...

What about to join forces and develop only one router lib which will just work as we expected? I think we can sketch some specifications, wishes, etc. I not too familiar with react router, and not sure we must look back to react at all, so it will be very helpful to get any comments from developers who used any routers much more and knowns usual problems etc.

If we have some specs, we just code it and will happy :)

Thank you!

boogerlad commented 4 years ago

my 2 cents: I've tried mduclehcm's router and sadly it does not behave nicely with nested routes. Also, seanmclem's router doesn't work with the latest solid. Haven't tried rturnq's. Just taking a brief look at it, it relies on path-to-regex (yours and mduclehcm's does too). While easier to write the paths, it doesn't lend itself to nested paths without duplicating the prefix. I would recommend using regular expressions with named capturing groups instead. Parent routes can use substring or \/(?<rest>.*)$ so that the child routes can operate on the suffix alone.

I found nested Switch / Match and createMemo along with the Dynamic control flow with /*@once*/ to be far more flexible to deal with nested routes and prevent unnecessary repaints / component recreation. Also, the active class does not have to be bound to the path specified in the href attribute. I have an example implementation here. Note, it's quite messy and definitely more verbose than it needs to be.

ryansolid commented 4 years ago

2 things,. I'm in complete agreement about nesting. Given that we aren't diffing views nesting is even more important when considering things like shared headers etc. I think nested routing is absolutely essential.

I strongly advise against using "/@once/" or hoisted creation in the general approach. If people want to do that in their end user code they can if they must. It messes with creation order which will make it incompatible with Re-Hydration and it has repercussions with lifecycle as bindings now belong to the parent scope so they are always live even out of view. This can lead to unexpected hard to debug behavior, increased memory usage, and potential performance degradation.

I know since we are dealing with just DOM nodes people conceptually like this idea of caching the nodes and bindings. But no one does that and it works out just fine. Think about how you would achieve this in React. You wouldn't.

Of course there is an exception to every rule but it should not be the norm.

If there is a desire to share header in route that do not share path, VDOM libraries could only handle this without re-render if it is topmost and available from direct navigation. I'd consider if there was a way to handle this using nesting a different way. Virtual paths, invisible/empty segments, etc...

boogerlad commented 4 years ago

Totally agree Ryan. For 99% of the use cases, Switch / Match and createMemo are sufficient. It's super rare to have a child component persist across multiple paths that don't have a common ancestor.

rturnq commented 4 years ago

I wrote @rturnq/solid-router for a couple reasons. Primarily I needed a router that I understood well for use in a project I am working on at my company. Additionally it was a good exercise to learn more about solid and particularaly using suspense and transitions.

Coming from a react background, I am used to react-router. It is clearly a succcessful router and I like a lot about it but I always found frustration in handing nested routes and having to know what routes the component was going to be a descendant of so paths and hrefs could be set correctly. It's good to see others are concerned about this as well.

To solve that issue I designed this router around the idea that each route creates a new context which contains that routes's defined path,any route parameter values and a link to the parent context. This makes it trivial for routes and links to recursively resolve their relative path or href to their ancestors by walking up the context tree. Resolving to the application root is also simple enough using the common path semantic of starting with a /. At this point the router works in the limited scenarios I have used it in but there is more to do - testing and SSR support are obvious needs.

To the original prompt, it would be great if solid had one idiomatic router. Evaluating the existing art and launching a discussion is a great start. I also think it will be hard to satisfy everyone. After framework and state management, routers are one of the most influential components of an application, informing much of the structure and design decisions.

I would like to see a router with the following:

aleksanderd commented 4 years ago

I cleaned the code a bit and create a npm package: https://github.com/zxlib/solid-router

Examples with nested routes here: https://github.com/zxlib/solid-router-examples

I'll be grateful for any feedback.

nested routing is absolutely essential

I noticed several nuances:

what else is worth paying attention to? in my examples, it seems to be most ok with nested routes, but I not tested named params patterns.

Parent routes can use substring or \/(?.*)\$ so that the child routes can operate on the suffix alone.

Agree.. I played several variants and this one seems to be simplest and pretty enough. Also, it saves ability of using named params from path-to-regex.

Agree, the router is not a place to cache. Is is better if it simpler.

One more place, which I was thinking - ability to configure/provide routes as json/objects via switch prop. It requires merging it with children inside the switch. So, I removed it in last commits. Also, I removed mapArray and I not sure in it... Can we think the switch's children array is immutable? If yes, the configurable routes can be made via some wrapper which render switch/routes by config or so.

Additionally it was a good exercise to learn more about solid

same here ;)

Support for route nesting and multiple "active" routes

you mean active route parents tree? or multiple active routes at one level? what will be the source/path for such matches? if url params, why not to move such logic to app components?

Isolated from any browser specific APIs.

Is the history lib enough for it?

joskoanicic commented 4 years ago

May I suggest that you take leverage from this library https://router5.js.org/ , it has all the router logic solved (and IMOHO better than react-router), it would just need the UI integration implemented.

ryansolid commented 4 years ago

Hmm.. does it support nesting? That's probably the single most important feature. In the past I've used Route Recognizer which is the basis of Ember's router, which was built to handle nested routing. It's a bit lower level though and doesn't handle the actual routing. I didn't think of it for this because it works generally works better when routes are defined top level. React Router v1 was like this but diverged after. But figured worth adding to the conversation.

EDIT: Hmm.. as to router5. I see an options.base in the browser plugin so I suspect multiple routers with base set could work. There might be another way.

aleksanderd commented 4 years ago

does it support nesting?

router5 supports nested routes like this:

const routes: Route[] = [
  { name: 'index', path: '/' },
  { name: 'route1', path: '/route1' },
  {
    name: 'users',
    path: '/users',
    children: [
      { name: 'view', path: '/view' },
      { name: 'list', path: '/list' },
    ],
  },
];

but I not sure you meant that...

...so I suspect multiple routers with base set could work

important feature is nested routes or nested routers?

ryansolid commented 4 years ago

Nested routes. Ok cool.. I kept looking for the term nesting and didn't see anything. They called it a tree of routes. Without finding it I was trying to think of ways to achieve it without this feature. This looks like it will suffice.

aleksanderd commented 4 years ago

using router5:

https://github.com/zxlib/solid-router5 https://github.com/zxlib/solid-router5-examples

ryansolid commented 4 years ago

@aleksanderd what's your opinion after putting that together? I mean you've done a couple routers now. I think the hardest thing here is actually choosing an option. Getting SSR and Suspense etc included should be simple by comparison. Something like React Router feels the most idiomatic to me so I'm a bit bias.

For all of you who wrote your own routers @aleksanderd @rturnq @mduclehcm @boogerlad @Seanmclem, I know a few of you were simply first there, but for the rest did you look at the other ones and see something that was missing. The basics we know we need are:

All route resolution should when detecting same parts should not recreate anything. Especially important with nested routes.

From there I consider anything else like SSR or Suspense 2nd tier. Do any solutions today do all of the above? Do all of them? It would be great to have a project or 2 that I can just point people to for routing.

aleksanderd commented 4 years ago

what's your opinion after putting that together?

By using some ready framework agnostic router lib we can have working router much faster.

The router5 lib seems to be works ok for most things. It have some for transitions, ssr, etc. But there are some ambiguities and misunderstandings for me. For example, I found two ways to get the route state: the router instance method getState(low level) and event based pair (route, previousRoute), I created an issue about it here: https://github.com/router5/router5/issues/468

I think the hardest thing here is actually choosing an option.

Agree. Even for a "ready lib" here several options...

Have no much free time to test all of them :(

On other side, any "non-solid" lib will be with some overheads. The solid signals allow to create more compact lib with better performance. For ex., from router5 issue above, it was tempting for me to not use their events at all, but attach a solid signal to the state getter/setter instead.

Anyway, there is one more fork here: how the routes declared - by jsx components or by top level config? Will the logic based on paths/patterns or on routes ids/names? I think jsx is not too good place for defining routes and second way (with clearly defined configs at one place) can help to simplify many things, more stable, more performance, etc...

The basics we know we need are: ...

Thank you for the checklist. Will play with it later... Also, it will be very helpful to specify this topics with more detailed descriptions, use cases, examples, etc.

mikeplus64 commented 4 years ago

https://github.com/mikeplus64/solid-typefu-router5

My contribution, this one is geared towards use in TypeScript specifically, and uses lots of TS tricks to get a type-safe Link and a type-safe Router component without much effort from the user of the library. (It consumes routes written in the usual Router5 way and adds lots of typing to it.)

The Router part is pretty neat, you write a tree where each key corresponds to a route name segment (like profile in profile.self) and each sub-tree renders (by an attribute called render or alternatively fallback) according to what the current route is. It also has a feature that let's you use routing to merely gather props to pass into a "owner" component, using the same process of traversing the tree according to what the current route is, instead of having different invocations of the same component across different routes, which should allow to avoid re-rendering.

e.g.

<Router>
  {{
    fallback: UnimplementedRoute,
    render: passthru,
    home: { render: Home },
    logged_in: { render: Home },
    profiles: owned => owned({
      render: Profile,
      defaultProps: {},
      props: {
        self: { id: () => 'self' },
        other: { id: () => route().params.id },
      }
    }),
  }}
</Router>

Where Router is typed specifically towards your applications routes.

More or less untested except in the package I split this out of, thoughts, prayers, and PRs very welcome.

ryansolid commented 3 years ago

Ok I compiled all the routers into a docs section(https://github.com/ryansolid/solid/blob/master/documentation/resources/projects.md#routers). I think that will make it easier to look at the options. I'm also creating a router for the isomorphic case to do metaframework file based routing(https://github.com/ryansolid/solid-app-router#readme). I'm prioritizing that case and building for size and features that are important to achieve that so I can't promise it will be the most fully-featured.

So the thing is I don't think the router I'm making is necessarily the best router for every type of application so I want see better router patterns emerge. I think these solutions are all great. There doesn't need to be a defacto one. Or if you feel that is important to have I guess you can treat mine as such since I'm committed to supporting it as part of Solid's success until I have reason to do otherwise.

MrFoxPro commented 3 years ago

I wish https://github.com/mikeplus64/solid-typefu-router5 will be recommended to use with solid and maintained by solid core team as it is really convenient and most production-ready router library.

guido4000 commented 3 years ago

Too bad Router 5 is not being maintained anymore. The last commit is more than one year ago and there are 20 open pull requests.

A simple and well maintained router is Navigo. I wonder how complicated it would be to integrate it with Solid. https://github.com/krasimir/navigo

ryansolid commented 3 years ago

The solution for Solid's Official Router has become clear for me now. I had it mostly right with Solid App Router and its nested routing and parallel fetching. After seeing the Remix Run demo it validated the approach I've been cooking up the past year. We're basically already doing a lot of the same things Remix Run does. However, looking closer at React Router 6 made me realize we can have the best of both worlds. We can take the core approach of Solid App Router and more standard React Router approaches and put them together into a single Router.

There are still details to work out but I've already reached out to @rturnq about working on this and consolidating our routing efforts. If anyone else is interested in this come say hi in the Discord.

Keats commented 3 years ago

Will the routes be defined in JSX? I've never understood that part of React Router and always ended up using router5 instead since the whole switch/render could live in its own function (and a mobx store/middleware in my case to keep track of it).

ryansolid commented 3 years ago

@Keats Both. This preview for the still unreleased React Router 6 gives an idea what I'm talking about: https://reacttraining.com/blog/react-router-v6-pre/. I was working already on the Ember, outlet style router where they aren't in the JSX and that is what Vite starter is built on etc.. But this seems to be an approach where we can do both. The routing in JSX and it's co-location has benefits for certain apps especially when you don't have file based routing.

The biggest win I see here is pointing people to a single solution and being able to leverage the community towards a single project. I didn't see how we could merge the approaches before. But now that I do I'm much more onboard with consolidating.

ryansolid commented 3 years ago

Ok solid-app-router has a new version. I need to writes docs but it unifies both approaches and is the product of both mine and @rturnq work. Honestly this has come full circle. So I'm considering this issue to have run its course.