solid-router icon indicating copy to clipboard operation
solid-router copied to clipboard

[FEATURE REQUEST] Add support for middlewares

Open Exelord opened this issue 3 years ago • 11 comments

Hi Ryan,

What do you think about adding support for Navigation Guards in order to allow middleware implementation like authorization.

Vue did it this way: https://router.vuejs.org/guide/advanced/navigation-guards.html#global-before-guards

While I like all the possible hooks they deliver, having a beforeEnter on the route definition level could be already a life saver, and open tens of door for middlewares. The meta property with router events like willTransition and didTransition will be also a great replacement.

Exelord avatar Jan 24 '22 18:01 Exelord

I've used these patterns before and implemented them in my other routers. I want whatever mechanism to play nicely with Suspense/Transition so having arbitrary especially async capable hooks is awkward. I agree there is something missing here even if just documentation as while data functions handle the typical cases I'm sure missing something. The idea here is to be non-blocking. You should probably view the data function as beforeEnter or any component level management. I think we probably could expose a few things for global hooks but I don't see things resembling exactly this.

ryansolid avatar Jan 25 '22 01:01 ryansolid

I'm not sure if I'm missing something but how would I handle route guards that need to be async and wait for a promise to resolve first?

In my case I need to await a function that gets an user asynchronously. The route should wait until the promise is resolved and then either load if there is an user or navigate back to sign-in if not.

Would that be an use-case for such middleware functionality?

m4rvr avatar Feb 06 '22 19:02 m4rvr

I've been thinking about this and I believe we can do something here. I'll lay out my thoughts and see if they make sense.

Expectations

The middleware feature should:

  • be optional opt-in
  • be asynchronous (ideally allow synchronous as well)
  • prevent rendering a route based on response/thowing/rejecting
  • allow redirecting
  • not block data functions (the data could be used for middleware logic)
  • be composable
  • tie into concurrent transition

Proposal

  • Add a new property willEnter/onMatch/middleware/? (TBD) to route definitions (ie. <Route />) which accepts a function. The function will take essentially the same parameters as the data function:

    • data: value returned from the route's data function
    • location: current location which is about to be rendered
    • params: path parameters from location
    • navigate: function to change the location

    The function will allow returning false or a void promise.

  • When a navigation occurs, the winning match (composed of one or more routes) will be determined. After the data functions have be called as normal, each middleware function will be called starting from the root, working toward the leaf. Note: Unlike data functions, a route's middleware will be called for each navigation where that route is part of the match.

    • If the function returns false (synchronously), the location will be reverted and no further middleware functions will be called
    • If the function returns a promise, the promise will be collected and the next middleware function called
    • If the navigate function is called synchronously no further middleware functions will be called, the navigation will be handled and the process will restart.
    • Errors thrown synchronously will not be caught (TBD)
    • All other values returned will be considered acceptance
  • After all middleware functions are called, any collected promises will be combined via Promise.all and read from a resource so they can trigger or be included in the concurrent transition when the author uses suspense. (This bit might be tricky)

    • If any middleware rejects, the combined promise will reject and location will be reverted
    • If the navigate function is called asynchronously, the combined promise will be rejected, the navigation will be handled and the process will restart.
  • When all active middleware have accepted by either returned something other than false, or their promise has resolved, the new components will be rendered. Note: a bunch of hand-waviness here on how to isolate location-driven changes any already mounted components might see and prevent them from updating until acceptance.

Thoughts

There are a number of technical challenges which will have to be worked out and there are still some things to figure out, but hopefully this provides a good starting point for an implementation plan. I'd like to hear feedback on this design and API, or hear alternative proposals.

Finally, while we consider this feature and potentially implement it, the best place to currently do this sort of work is in a route component which wraps your target routes. Here is an example of a very simplistic protected route implementation. Asynchronous logic could be done in the same way by utilizing resources to determine when to show or redirect while pending state would have to be handled by concurrent transitions/suspense. Maybe I'll try building a more realistic example.

rturnq avatar Feb 07 '22 07:02 rturnq

@rturnq thanks for the detailed idea, I really like it and would love to have it implemented here!

Could you please show how the protected route component would call an async function so the route waits until it's resolved? Basically like described in my comment above. That would be superb and really help me!

m4rvr avatar Feb 08 '22 18:02 m4rvr

This issue has been really nicely solved in Ember.js in 2013. I think we can really inspire as every now and then I see those patterns being introduced as "Innovative ideas" in many modern frameworks. Vue is one of them, as well as React Router (in terms of API capabilities).

The most interesting things from it are transitions, beforeModel and router events.

  • https://api.emberjs.com/ember/release/classes/Transition
  • https://api.emberjs.com/ember/4.1/classes/Route/methods/beforeModel?anchor=beforeModel
  • https://api.emberjs.com/ember/4.1/classes/RouterService/events/routeDidChange?anchor=routeDidChange

Authorization, session load, blocking, data loading, navigation guarding... it's all solved thanks to these concepts.

Exelord avatar Feb 08 '22 18:02 Exelord

Yeah the non-blocking transition model is where the challenge comes in. I'm a big fan of Ember Router. The webcomponent router I implemented back in 2014 was a nested router with these same middleware hooks very inspired by Ember router. So I'm super familiar with the patterns there. But this is also a very different world.

We need to consider why people are using these middleware and what patterns that promotes. The biggest thing is that with non-blocking patterns generally speaking you don't guard the page, you guard in the page. You need to consider this due to the lack of data anyway, so like if you hit a route and you can't synchronously determine if they are unauthorized it is no different than not being able to synchronously determine the data the route needs. And can be handled very similar to how one would handle a data error. Of this doesn't need anything special. Mind you it is common to section off a whole section of the app in which case having this logic in a shared parent route makes a lot of sense rather than spreading it around. This is largely why I don't see the need for the beforeEnter hook type things. You add another mechanism to do similar things. Does it really need to be blocking.. no. The beautiful thing about non-blocking is you always have the ability to block. But vice versa that isn't true.

The other thing which was cool was Ember had this will transition idea so that any handler on the page could prevent navigation when it was triggered asynchronously. This was really cool I have to admit and something that I wouldn't mind seeing back. It is interesting mind you because at that point is it routing or not? Maybe it doesn't matter.. Usually you'd use this to show like the, "Work unsaved do you want to continue?" I think this worth considering as that is really powerful feature and is hard to implement other ways.

ryansolid avatar Feb 09 '22 01:02 ryansolid

Is There any update on middlewares? I badly need this feature.

ansarizafar avatar Aug 11 '22 07:08 ansarizafar

After trying multiple workarounds this one is the only one I got that's working:

create a function in your project like this

export const AuthGuard = () => {
    const navigate = useNavigate();
    onMount(() => {
        const auth = useContext(AuthContext);
        if(auth.auth){
            navigate("/Dashboard", { replace: true})
        }
    });
}

and then called in the first line of the page component you want to apply the middleware to it.

as any other place you try to put the function in like in the component part or the element part of the router, it'll be working but the context won't load.

also trying to wrap the component function itself with it (like HOC in React) will give you the same result with the context not loading any data.

btw we really need the guard or middleware functionality in Solidjs as it's a mandatory functionality for any medium to large project (talking here about authentication and authorization which are essential parts of any project)

SalemCode8 avatar Sep 24 '22 03:09 SalemCode8

I'm also a fan of Angular's CanActivate / CanLoad route guards that trigger before navigation starts. (CanLoad actually prevents the lazy module loading from triggering at all, something that I don't believe Solid Router is capable of in its current solution)

A benefit to doing this before you wind up on the page and then waiting for async stuff is that it's kind of a bad user experience if someone clicks on a link, they're navigated to a a page full of spinners then bounce to a login page. Especially with lazy loaded modules where we don't have the "CanLoad" support so now we have to download the module, let it bootstrap, then let it realise it got no authorisation in its data payload and then bounce them, making the whole process slower due to network calls for component modules

I'm still very new to Solid, and I skipped React completely so I'm freshly learning a lot of the patterns here both Solid unique and the ones it's inherited from React, so possibly I just haven't gotten used to these ways yet, but I'd say there's still a lot of benefit to having a blocking mechanism for routes before navigation starts, instead of just relying on every protected component path to have to check itself after load. I feel like the route config should control how the components are loaded, not components control how they load themselves. It makes sense to copy paste "IsAuth()" into a dozen router config entries (or ideally just the top level of a few parent component router entries) than it does to copy paste the IsAuth() into every component that may need to be protected.

Edit after thinking a bit more: I guess the intention here from @/ryansolid is that the entire page would be wrapped in a Suspense block depending on that IsAuth data payload? That would prevent the multiple spinners, you just have a generic page loading state the Suspense triggers.

Something I haven't quite worked out yet is how you pass in a data Function that returns multiple units of data. data isn't really the same NG's route resolver where you have multiple properties with unique resolvers on each. I guess you could have your data function return multiple resources, or have one mega store stitched together from multiple resource calls, but getting data out of the data function is still a tad confusing and I'm not sure how you'd sequence complicated route payloads if you need to load user authentication, then load 3 or 4 different data sets inside the page from different endpoints. Maybe the argument is that the components that need that data should do the load themselves and not ask the route for it, but then we're back to the multiple Suspense in a page problem

rickdoesdev avatar Nov 06 '22 00:11 rickdoesdev

I make is somehow work with router check my fork at least https://github.com/puneetxp/solid-router check my readme.md for guard that will many thing. I am still very new to contribute to another code please check if you find any error let me know.

I just create a memo which should deal with the can return true or string. String mean it have to redirect. true mean OK good to go. That function run on component level can detect login status change that way as i needed that way.

Again I work for solid for just 7 days i know very little hope we can help our-self i submit a feature request. A Maintainer can do till the point only.

We can do some update to router with path of redirect if we want but I lack clarity that time.

puneetxp avatar Nov 14 '22 20:11 puneetxp