React.js and browser history: a history.js mixin
So you’ve written a snazzy interface using React and you’re pretty happy with it, but while playing around with it you hit your browser’s back-button, and
suddenly you’re on a completely different page, huh?
The problem is that although React is updating and keeping track of its state
internally, this information is never communicated to the browser, and so the
browser has no concept of what state your React components are in.
To mitigate this I have written a mixin for use with
history.js which allows you to
save the current state of your UI in the browser’s history, and that way allow
you to navigate back and forth in the state of your application (history.js
is
an impressive piece of software, which makes manipulating the browser history a
breeze, without it I would never have been able to write this).
Simple working example #
Here’s very simple example of using mixin
var SimpleCategoryView = React.createClass({
mixins: [HistoryJSMixin],
getInitialState: function() {
return { current_category: 0 }
},
handleCategoryChange: function(e) {
this.setState({current_category: e.target.value});
this.saveState();
},
componentDidMount: function() {
this.fetchCategoryData();
this.bindToBrowserHistory();
},
hasRecoveredState: function() {
this.fetchCategoryData();
},
fetchCategoryData: function() {
// here'll be an Ajax-call to populate the component's state
},
});
In this example we’re interested in restoring which category a user has picked, when the user presses the browser’s back (or forward) button. Since the component’s render
method will probably rely on the category always having a value we define an initial value with getInitialState
.
This second method handleCategoryChange
is a simple React-style handler for when the user changes the category through the UI, but with an
extra function call saveState
, this call causes the mixin to push the state to
the browser’s history. Finally, if a method called hasRecoveredState
exists,
this will be called when the state is restored from the browser history, I used
it here to perform an ajax call to fetch some data from the backend.
The above is a complete example which takes the entire internal state of the
react component and stores it into the browser’s history, and you can use it just like that.
Advanced usage #
There are however two considerations which may require you do define a few more parameters:
- In the example above the entire state of the React component is saved. This is not a problem if there are only few a variables, but if the state is a large dictionary for instance this may cause trouble. This is because many browsers put a character limit on the size of the state which is stored, for instance firefox limits the stored state to 640k characters.
- You may be using other datatypes in the internal state than just the javascript primitives, I was for instance using moment.js. When we ask the browser to save the state, it wil try a serialize the state, however it may not know how to correctly serialize and de-serialize the state.
To solve these two problems I added two optional methods that will be used by the
mixin if they exist, serializeState
and deserializeState
. The first method allows you to
define exactly which state variables should be saved and how they should be
stored, and the second method then defines how to deserialize the state.
Imagine that SimpleCategoryView
has many more state variables that just
current_category
, if I only wish to save the category into the browser history
I could add the following to the example above:
var SimpleCategoryView = React.createClass({
...
serializeState: function() {
return {
current_category: this.state.current_category,
}
},
});
As an example of a derived datatype I will use moment.js. Since moment.js
serializes itself when a string representation of the object is requested I
need to only define the deserialization, like so:
var SimpleDateView = React.createClass({
...
getInitialState: function() {
return {
start_date: moment(),
}
},
deserializeState: function(state) {
state.start_date = moment(state.start_date);
return state;
},
serializeState: function() {
return {
start_date: this.state.start_date,
}
},
});
I have put a full working example on github: https://github.com/leifdenby/jshistory_react_mixin
The mixin uses jQuery, and the version of history.js
I have used is the one
bundled for operation with jQuery.