History Store In Flux

Feb 27, 2015

UPDATE: There will be a follow up post to explain how to use React Router with Flux which is a much better and production safe option.

While using Flux to spike an application for work we needed to have push state/history.

While there is React Router which can do this for you (with a bit of tinkering for Flux); I decided to try and do it myself in a Flux way, using Actions and Stores. It works quite well and other stores can listen out for the history store dispatch token. Meaning that if another store needs to utilise the current state then we know it will be the latest state.

My implementation at the time of writing it is rather basic but for the purpose of the spike it was more than enough.

Our HistoryStore will use window.history.pushState to implement our ‘history’ and at the time of writing this, the logic was held in the Store. If I was re-writing this now I would probably put the pushState logic into the Action as the Store should not worry about this: only the data it holds.

The Store

router.js
1var HistoryConstants = require('../constants/HistoryConstants'),
2defaultPage = '/',
3_currentPage = defaultPage;

defaultPage we set to be what the first route we expect will be. This is a bit nasty as this could depend on certain factors, but we would fire off an action to update the current page in these cases.

_currentPage is a private variable we will use to store what page we are in the app. This can then be queried in the public API below if we need it in any component functions/stores etc.

router.js
1function updateHistory(page) {
2 _currentPage = page;
3 window.history.pushState({ page: _currentPage }, 'test', 'index.html#/' + page);
4}
5
6function updatePage(page) { _currentPage = page; }

These private functions we will use to update the current page variable, so these are our setters.

The reason we have two is one will update the history (pushState) but one will only update the current page. This is needed as if a user navigates to a URL or enters the flow at a different point via a bookmark for example, then we need to update the current page but not the history.

These could probably be merged into one function but I just wanted to keep it mice and verbose what each method does.

router.js
1window.onpopstate = function(event) {
2 if (event.state) {
3 _currentPage = event.state.page;
4 } else {
5 _currentPage = defaultPage;
6 }
7
8 HistoryStore.emitChange();
9}

This function is a hook onto the window.onpopstate function which looks after the user going back. If this happens we also want to update the page. Moreover, as this is caught by the window we have to fire the an emitChange so that the stores listening for history can update themselves accordingly.

router.js
1var HistoryStore = assign(
2 {},
3 EventEmitter.prototype, {
4 getPage: function() {
5 return _currentPage;
6 },
7 }
8);

Then we have our getter which simply returns the current page.

router.js
1HistoryStore.dispatchToken = AppDispatcher.register(function (action) {
2 switch(action.actionType) {
3 case HistoryConstants.UPDATE_HISTORY:
4 updateHistory(action.page);
5 HistoryStore.emitChange();
6 break;
7 case HistoryConstants.UPDATE_PAGE:
8 updatePage(action.page);
9 HistoryStore.emitChange();
10 break;
11 default:
12 return true;
13 }
14});

Our AppDispatcher.register function looks out for UPDATE_HISTORY and UPDATE_PAGE actionTypes and calls the functions above depending on the action and then emits a change event.

router.js
1module.exports = HistoryStore;

And finally export it!

The Actions

router.js
1var HistoryConstants = require('../constants/HistoryConstants');
2
3var HistoryActions = {
4 updateHistory: function(page) {
5 AppDispatcher.dispatch({ actionType: HistoryConstants.UPDATE_HISTORY, page: page });
6 },
7 updatePage: function(page) {
8 AppDispatcher.dispatch({ actionType: HistoryConstants.UPDATE_PAGE, page: page });
9 }
10};
11
12module.exports = HistoryActions;

Other Stores

Then in our store that we need to access the current page state we can wait for the HistoryStore to have updated the current page:

router.js
1AppDispatcher.waitFor([ HistoryStore.dispatchToken ]);

How To Call It

router.js
1handleClick: function() { var nextUrl = StepStore.getNextUrl(); HistoryActions.updateHistory(nextUrl); }

The history is updated in something like a click handler for a link. In the app that I was building we don’t put the URL on the item the user will click on as we need to work out the next step based on some logic. This is quicker to do for the item the user has clicked on, not all of them on render. Therefore in this function we work out the next step url then call the updateHistory action.

Contact.

LET'S WORK

TOGETHER

I am open to both contract and freelance developer projects and would love to hear what your ideas are.

Feel free to drop me an email and I'll let you know how I can help you!

mail@benstokoe.co.uk