Sync React Redux State with Browser Events

Full Stack Web and Mobile Applications with React, React-Native, iOS and Android

Sync React Redux State with Browser Events

React Redux App State with Browser

React is such an amazing tool, but one of the worst things is trying to keep a hundred different libraries in sync with each other. I recently went on an excursion in the realm of state persistence with browser navigation. Ultimately, what was happening was that if I clicked the back button, for example, the title in my app bar would not change back to the respective title for that page. I use react-router-redux to keep application state in sync with the router for this app. I also have the withRouter higher order component from the react-router library wrapping the export of my App component where my routes live. I guess I just assumed that the corresponding state for the specified hash change on the browser navigation event would repopulate.  As it turns out, even to this day web browser’s APIs still suck. Some don’t even have a way to programmatically detect for browser navigation. According to the chrome.webNavigation API documentation,  you can listen for a forward_back event, but even then, which one is it? Forward or Back? And these things are inconsistent between browsers, and sometimes even nonexistent.

My mistake was thinking that react-router-redux could detect such changes. For one, the react-router-redux documentation is mostly outdated, and in trying to find information on syncing the browser navigation with the redux store, all of the solutions on stack overflow, blog posts, and tutorials, pointed to using the react-router-redux function called syncHistoryWithStore.   Come to find out – syncHistoryWithStore was deprecated after version 4.0.8. After googling for solutions to that problem, all I could find was people saying to use that version instead of the latest version of the library as their solution. So I tried it. After replacing the current version with version 4.0.8, I load my app, and…. nothing.  The architecture of my app depends on certain pieces of the version 5 library of react-router-redux, which I quickly found out were non-existent 2 years ago with 4.0.8. Either that was the issue or the implementation of the library was different, or most likely – both. Like I said though, documentation is poor so tracking down what a 4.0.8 implementation might look like? Forget it. I went through a lot of other posts on stack overflow and blog postings, with people claiming that you can do, and I implemented about 12 different solutions before I took a step back for re-evaluation because nothing was working, which then led to my final solution.

I realized that my assumption was flawed in the first place. You might not want application state to sync with the hash history because a change in state on one page that effects the previous page, then clicking the back button, would cause that state to be replaced by the older version that did not have that change. Nevertheless, it was what I needed in this instance. However, I ended up finding a solution to the selective state update conundrum as well, which you will see in a moment.

My initial solution involved the caching of state data changes and waiting to save them to sessionStorage because I didn’t want to be running JSON.stringify on every state change. I found a much better solution to that as well, why I am about to show you..

Add Event Listener and Save the Session

If you need state to persist so that you can reload that version of state as the result of browser navigation, the best way to do it is to create a saveSession() function that is triggered by the window unload event, like so:

window.addEventListener('beforeunload', this.saveSession)

Note that if you really want to use jQuery, you could also do:

$( window ).unload(function() {
  saveSession();
});

Next thing you want to do is save the data to sessionStorage. Note that I am not suggesting the use of localStorage in this instance because we are talking about loading the corresponding state between browser navigation events within the same window. We are also assuming here that you’re not trying to load that particular state later or a few days later. You will want to use localStorage for that, which does not go away unless explicitly deleted in the application code, or by the user. For this use-case scenario, we’re just talking about holding the corresponding state for this session; when the window is closed, sessionStorage is deleted. Also note that this solution will preserve the state on browser refresh as well as navigating away from your app to another web site and then back to yours (like if you forgot to set an external link to open in a separate tab). Your saveSession will look something like this:

export const saveSession = (state) => {
  const serialized = JSON.stringify(state);
  window.sessionStorage.setItem('state', JSON.stringify(serialized));
}

Subscribe to State Change with Redux

Now we’re going to use redux’s handy subscribe() method to listen for changes to state and any time the store is modified, saveSession() will be called.

store.subscribe(() => {
  saveSession(store.getState());
})

Note that with this method, you are also holding onto the application’s entire UI state, which is actually what I wanted. However, a lot of the time you may want to selectively populate data in the UI and reset some components. For this you just need to be specific about what you save, like so:

store.subscribe(() => {
  peopleDTOCollection: saveSession(store.getState().peopleDTOCollection);
  newsItemsDTO: saveSession(store.getState().newsItemsDTO);
})

As much as I’d like to, I can’t leave it at that with a clear conscious.  You might be already thinking about the idea of calling the saveSession function on every single state change and the fact that the JSON.stringify method, called a million times in a row, could impact the apps performance. This made me weary about implementing this solution initially. After all, performance is the name of the game in our world, so we’re going to solve this pesky problem. Initially my solution involved caching items and periodically saving them, but it was more code than I wanted to have to write, so I turned to google, and in came lodash to the rescue.

Throttle Consecutive Save Operations with Lodash

There is a handy method in the lodash library called _.throttle that is meant to solve this very problem. Lodash is an amazing utility library, so its worth doing an npm install --save lodashif you don’t already have it installed. Next import throttle from 'lodash/throttle'and add it to your store.subscribe as follows:

store.subscribe(throttle(() => {
  saveSession({
    peopleDTOCollection: store.getState().peopleDTOCollection);
    newsItemsDTO: store.getState().newsItemsDTO);
  })
 }, 3000)
))

Finally you implement your loadStateFromStorage() method, which you may call from your App.js componentDidMount() lifecycle method. Here’s how this should look:

export const loadStateFromStorage = () => {
  const serialized = window.sessionStorage.getItem('state');
    if(serialized === null) {
      return undefined;
  }
  return JSON.parse(serialized);
}

We have to do a null check and return undefined on window.sessionStorage.getItem('state') because more times than not, there isn’t going to be anything there. Remember that once that browser window closes, sessionStorage is purged.

I hope this will help some people to avoid the painstaking process I had to go through trying to do this with react-router-redux and react-router (among other supposed “solutions”), before I gave up on that and just wrote it myself. Its a good lesson – not to become so dependent on libraries that you immediately turn to them when you could accomplish the same thing in much less time by writing the code yourself.

 

 

Leave a Reply

Your email address will not be published.