Choo: onRoute hook

Created on 10 Jul 2016  路  14Comments  路  Source: choojs/choo

For certain things like authenticated routes, it would be useful to have a function that runs before each route change so that authentication can be checked and the user can be redirected. Something like:

const app = choo({
  onRoute (route, state, prev, send) {
    if (route.auth && !state.authenticated) {
      return send('location:setLocation', { location: '/login' }) //redirect
    }

    // otherwise default behavior
  }
})

app.router((route) => [
  route('/login', { auth: false }, loginView),
  route('/protected', { auth: true }, protectedView)
])
app.start()

Most helpful comment

Cool idea. Do you think it provides value over just wrapping your routes? ie.

const auth = (view) => (state, prev, send) => {
  return state.authenticated ? view(state, prev, send) : loginView(state, prev, send)
}

app.router((route => [
  route('/', homeView),
  route('/account', auth(accountView)),
  route('/purchases', auth(purchasesView))
])

All 14 comments

as discussed on IRC, def sounds like a useful addition

Cool idea. Do you think it provides value over just wrapping your routes? ie.

const auth = (view) => (state, prev, send) => {
  return state.authenticated ? view(state, prev, send) : loginView(state, prev, send)
}

app.router((route => [
  route('/', homeView),
  route('/account', auth(accountView)),
  route('/purchases', auth(purchasesView))
])

@timwis that's a cool way of doing it, didn't think of that.

Another related feature that onRoute is useful for is changing the page title on a route change:

const app = choo({
  onRoute (route, state, prev, send) {
    if (route.auth && !state.authenticated) {
      return send('location:setLocation', { location: '/login' }) //redirect
    }

    document.title = 'AppName - ' + route.title // could be cool to incorporate params too...
    // default behavior
  }
})

app.router((route) => [
  route('/login', { auth: false, title: 'Login' }, loginView),
  route('/protected', { auth: true, title: 'Protected' }, protectedView)
])
app.start()

Not sure what the best way with the wrap approach would be.

Yeah that would be handy. I imagine that could be done in the view's onload callback, though it'd be annoying to have to do that for every view.

I was just about to create a new issue, but I think my case is similar enough to this one:

Basically I want to initialize the model with an asynchronous call (e.g. XHR). I played around with an onload handler, but ended up creating a subscription that calls a reducer with the data and passed on done.

I can't tell whether there's already a better / more clear way to do this or if that's something that could be addressed with this feature?

@mantoni Hmmm, the way I view this is:

  • If it's a single view that calls a specific endpoint once it's loaded, the onload hook should be the way to go; probably calling an effect in the process.
  • If every view is calling endpoints when they start up, creating an abstraction might make sense.

@yoshuawuyts Can you explain where you see this being different in the onRoute case? I understand that the case being made here could also be solved in onload?

@yoshuawuyts Sorry, ignore me. I just saw that onRoute is a feature of the choo function. I was confusing it with a model feature. It's all clear now. Thanks.

Hmmm, though on the other hand... this might make choo a bit more frameworky - less hard to figure out what to expect. Maybe we should not make it this way? Or maybe we should call it wrapView() and put it in line with #145 ? idk - not quite sure yet what the best way of approaching this is

Glad the solution @timwis proposed should at least provide a workable experience right now :)

For discussion, here's another use case: logging. In my apps I'll often want the browser to tell the server which routes/params are being used.

@davidguttman hmm, that should already be possible with the onState() hook, as the current path is simply a value on the state

Came here based on the use case of a header marking a nav element as current, or active, based on where one was in the app. I could read it based on app.state.location, after I clean it up, or it'd be neat if I could just listen to it as a effects, perhaps?

(Edit: I meant to write effects instead of subscriptions, currently namespacing doesn't allow this AFAICS)

Hey @saem,

I also spent some time figuring out how to set my active menu item. I kept getting into situations where I changed the state and triggering an infinite loop.

I'm using a sidebar and only want to render the content in the area next to it, to do that I was already wrapping my views in a function like @timwis did with the auth example. It was then a matter of passing also the route name to that function and do another check.

  app.router('/', (route) => [
    route('/', subview(views.home, '/')),
    route('/about', subview(views.articles, '/about'))
  ]);

const subview = (view, route) => (state, prev, send) => {
    const { activeRoute } = state.sidebar;

    if (route !== activeRoute) {
      send('sidebar:setActiveRoute', route);
    };

    return views.main(state, prev, send, view)
  };

I have a model for my sidebar which keeps track of the active route, menu items and whether or not it's expanded (mobile).

It seems to do the job pretty good but I'm sure there will be a more elegant way of doing this. If anyone has one please shout out.

Cheers!

I think this question is answered. Thanks for opening!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rafaismyname picture rafaismyname  路  6Comments

corderophi678 picture corderophi678  路  5Comments

tunnckoCore picture tunnckoCore  路  3Comments

wenkesj picture wenkesj  路  3Comments

tornqvist picture tornqvist  路  4Comments