React-Router with Flux
react-router provides great route handling with route params and query params. Flux provides a pattern for building React apps, including a pattern from providing data to your components. Here's a straightforward way to make route changes trigger data changes in your components.
Note: Code here reflects usage of react-router 0.11.x.
It is often the case that we'll need to trigger data changes in a react component in our Flux app because of a route transition. A prime example of this would be on an object show page, where the details of a particular object are being shown. In our example, let's say that we're showing the details of a book, such as title, author, and description.
The route to our book detail page is likely defined as:
// ...
<Route name="book" path="/books/:id" handler={require('./books-show')} />
The handler is just a controller-component that defines the view. It might look simply like:
const React = require('react')
const {State} = require('react-router')
const BooksStore = require('./books-store')
module.exports = React.createClass({
displayName: 'BooksShow',
mixins: [ State ],
getInitialState() {
return this.getStateFromStores()
},
getStateFromStores() {
return {
book: BooksStore.find({ id: this.getParams().id })
}
},
componentDidMount() {
BooksStore.addChangeListener(this._onChange)
},
componentWillUnmount() {
BooksStore.removeChangeListener(this._onChange)
},
_onChange: function() {
this.setState(this.getStateFromStores())
},
render() {
return (
<ul>
<li>{this.state.book.title}</li>
<li>{this.state.book.author}</li>
<li>{this.state.book.description}</li>
</ul>
)
}
})
Note that the view component references a BooksStore
for getting its initial state. The state of book
will also be updated as the BooksStore
emits that its data has changed and this._onChange
is called.
Every time a route transitions, react-router has a Router.run
callback that will also run in order to render the matching route. This will also be a great place to put our action to signal to our Flux app that routes are transitioning and therefore other stuff like data in the display might need to change as well.
Router.run(routes, (Handler, state) => {
React.render(<Handler />, document.body)
BooksAction.transition(state.params)
})
Important note: Make sure the transition action is called after React.render
so that the change emission will be detected after render, otherwise you'll be one route transition behind.
The BooksAction.transition
definition is something very simple -- something that can trigger events on the dispatcher:
exports.transition = () => {
AppDispatcher.handleViewAction({
type: ActionTypes.TRANSITION
})
}
The final connection is in the store, where the dispatched action can be listened for an then trigger the store event, updating the component views:
var BooksStore = merge(EventEmitter.prototype, {
find(filter) {
// ...
}
})
BooksStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action
switch(action.type) {
// ...
case ActionTypes.TRANSITION:
BooksStore.emitChange()
break
}
})
module.exports = BooksStore
At this point, all the route transitions should trigger data changes in views. Visiting the url /books/1
and then the url /books/2
should display different data on screen according to which book id was in the route. Router.Link
should work correctly, usable instead of buttons with actions being triggered on click.
react-router and Flux make for a great combo in this way, right? What adjustments would you make?