In Flux architecture, how do you manage client side routing / url states?
Asked Answered
E

2

43

As a follow up to the Store lifecycle question,

In a typical web app its nice to have a shortcut to the current application state via the URL so you can re-visit that state and use the forward and back buttons to move between states.

With Flux we want all actions to go through the dispatcher, which i guess also include an URL change. how would you manage URL changes within a flux application?

Exhortative answered 13/5, 2014 at 7:0 Comment(1)
Could you clarify what you mean by navigating between states in your question? I think you don't mean application state, but rather navigating between routes/SPA urls. If so, Flux is merely an application architecture which describes inter app communication and control. The Flux community has generally decided routing is out of scope, and see Nacho's answer re: react router.Manamanacle
H
43

[Update]

After working on a bunch of React/flux applications, I've come to the conclusion that I prefer for routing to be handled separately and orthogonally to flux. The strategy is that the URL/routes should determine which components get mounted, and the components request data from the stores based on the route parameters and other application state as necessary.

[Original Answer]

An approach I took with a recent project while experimenting with Flux was to make the routing layer just another store. This means that all links that change the URL actually trigger an action through the dispatcher requesting that the route be updated. A RouteStore responded to this dispatch by setting the URL in the browser and setting some internal data (via route-recognizer) so that the views could query the new routing data upon the change event being fired from the store.

One non-obvious piece for me was how to ensure URL changes triggered actions; I ended up creating a mixin to manage this for me (note: this isn't 100% robust, but worked for the app I was using; you may have to make modifications to suit your needs).

// Mix-in to the top-level component to capture `click`
// events on all links and turn them into action dispatches;
// also manage HTML5 history via pushState/popState
var RoutingMixin = {
  componentDidMount: function() {
    // Some browsers have some weirdness with firing an extra 'popState'
    // right when the page loads
    var firstPopState = true;

    // Intercept all bubbled click events on the app's element
    this.getDOMNode().addEventListener('click', this._handleRouteClick);

    window.onpopstate = function(e) {
      if (firstPopState) {
        firstPopState = false;
        return;
      }
      var path = document.location.toString().replace(document.location.origin, '');
      this.handleRouteChange(path, true);
    }.bind(this);
  },

  componentWillUnmount: function() {
    this.getDOMNode().removeEventListener('click', this._handleRouteClick);
    window.onpopstate = null;
  },

  _handleRouteClick: function(e) {
    var target = e.target;

    // figure out if we clicked on an `a` tag
    while(target && target.tagName !== 'A') {
      target = target.parentNode;
    }

    if (!target) return;

    // if the user was holding a modifier key, don't intercept
    if (!e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey) {
      e.preventDefault();

      var href = target.attributes.href.value;
      this.handleRouteChange(href, false);
    }
  }
};

It would be used as so:

var ApplicationView = React.createClass({
  mixins: [RoutingMixin],

  handleRouteChange: function(newUrl, fromHistory) {
    this.dispatcher.dispatch(RouteActions.changeUrl(newUrl, fromHistory));
  },

  // ...
});

The handler in the store might look something like:

RouteStore.prototype.handleChangeUrl = function(href, skipHistory) {
  var isFullUrl = function(url) {
    return url.indexOf('http://') === 0 || url.indexOf('https://') === 0;
  }

  // links with a protocol simply change the location
  if (isFullUrl(href)) {
    document.location = href;
  } else {
    // this._router is a route-recognizer instance
    var results = this._router.recognize(href);
    if (results && results.length) {
      var route = results[0].handler(href, results[0].params);
      this.currentRoute = route;
      if (!skipHistory) history.pushState(href, '', href);
    }

    this.emit("change");
  }
}
Heeley answered 13/5, 2014 at 16:14 Comment(4)
How about integrating routing into the dispatcher it self? Some of the events will be transformed into a route. Then when some one hit the route, dispatcher can dispatch the event related to that.Iridic
@BinaryMuse, what was the reason you came to your updated conclusion? I'm managing a larger Angular application that's fairly reactive, and am turning to FLUX purely for the purpose of trying to get a singular data flow. It would be interesting to know why you prefer a different solution for routing, as I'm just starting out.Raynell
@BryanRayner Flux stores essentially take events (actions) and reduce them down to changes in state, which are stored within the stores. Routing is basically this already: the browser history events are the actions, and the store is the current URL. I've found that adding additional complexity here doesn't seem to provide any benefits. Plus, with all the serializable state in the URL, it's clear what's saved there and what isn't.Heeley
That's true. We're building an Angular app where only a portion of the codebase uses a FLUX style architecture. Like you wrote in your updated answer, I've come to the conclusion that the router should continue to load controllers, templates, etc. as normal, and then those components call the neccesary actions based on the de-serialized URL parameters.Raynell
L
2

Most examples in the wild make use of React Router, a framework based on the Ember router. The important part is the configuration of routes as declarative specification of components:

React.render((
  <Router>
    <Route path="/" component={App}>
      <Route path="about" component={About}/>
      <Route path="users" component={Users}>
        <Route path="/user/:userId" component={User}/>
      </Route>
      <Redirect from="/" to="about" />
      <NotFoundRoute handler={NoMatch} />
    </Route>
  </Router>
), document.body)
Lectureship answered 23/9, 2015 at 9:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.