Reloading React State on Page Refresh

Ever refreshed your page in your React app? Seen things completely blow up? It's time to rehydrate that state!

Picture yourself building the “next big thing” in React. You’ve got everything set: your Redux stores are perfectly sculpted, React code is picture perfect, Actions are dispatching…it’s a beautiful thing.

Then you hit the refresh button. Things start breaking. Those API calls that relied on data in your user state just broke, because the user doesn’t exist in the state! What’s a dev to do?

Enter: “rehydrating”

Rehydrating is just one of those terms that we as developers like to throw around to make things seem fancier than they are. In actuality, it’s nothing more than loading some critical data in the state on a page load. For sake of argument when I say page load, I’m referring to an initial load (typing the address into a browser) or a hard refresh.

Let’s start with our user reducer and look at our initial state. For my example, it looks like this:

const initialState = {
        user_id: null,
        token: null,
        user_data: {}
    }

user_id will represent the logged in users id key that can be passed to my REST API. token is their access token that can be passed as a header to my API, and user_data is an object that contains info that my API returns to me when a user logs in - things like first name, last name, email, etc…

Switching initialState to a () => {}

This may be one of those “D’oh” moments to developers, but the trick here is to realize that initialState should be an object, but there’s no real restriction on how you get that object. So in my example, I’m going to set initialState equal to the return value of a function. That function is where the magic happens:

const hydrateUserState = () => {
    let defaultState = {
        user_id: null,
        token: null,
        user_data: {}
    }

    try{
        const loggedInUser = JSON.parse(localStorage.getItem('appUser'));
        if(loggedInUser === null){
           return defaultState;
        }
        const hydratedState = {
            user_id: loggedInUser.id,
            token: loggedInUser.token,
            user_data: {
                firstName: loggedInUser.firstName,
                lastName: loggedInUser.lastName
            }
        }
        return hydratedState;
    } catch (error) {
        return defaultState;
    }
}

Right out of the gate you can see that we’re setting a defaultState value. This is going to be our “something went wrong/something doesn’t exist” fallback version of our state.

Next, we have a simple try/catch as some browsers in certain security settings modes will throw an error when trying to use localStorage. In that case, we simply return the default state.

If we are able to get our object out of local storage we then check to see if it’s null - if it is we return the default.

If we are able to get our object and it contains data, then this is the fun part, we set the values of hydratedState and return that from the function.

So now that we’re set up, all that’s left to do is call that function:

const initialState = hydrateUserState()

A Caveat or Two

This way isn’t perfect, but it’s worked fairly well for me for simple rehyrdation (I have a few more ideas below). But this does assume that you’re storing some type of user data about your logged in user in local storage. If you’re not then this solution most likely won’t work for you.

Other ways to go about this

Now that we’ve got the basic idea and logic behind it there are a few more ways to do this / some tweaks you may want to make.

  • Store only a token and not a serialized object in localStorage. This allows you to take the token, hit your API / auth service to validate that the token is still valid and if it is, then hydrate the state with the user data. This is a method I’ve recently adopted and it seems to be more secure / cleaner to implement.
  • Let someone else solve the problem. redux-persist is a Redux add-on that does just this. You can specify certain state values to persist on refresh and it will store them in local storage and hydrate them on page load. More details: redux-persist