React: Redux and localStorage

Andrew Bone - Jul 30 '20 - - Dev Community

This weeks in my React adventure I've been looking at how to untangle the spaghetti of passing around states using Redux, and react-redux, while I was there I looked at storing the Redux store in localStorage.

Benefits

Let's imagine a scenario where we have 2 components, a to do list to display items and offer some interaction, such as delete, and an entry form that allows you to add items, not an uncommon scenario.

You'd probably make a wrapper component that had a state containing the list and some functions to edit the state which we can pass down to our components using props.

That seems simple enough but now let's say we have another page that is a filtered down list, let's say it only shows items marked as complete, how would you get access to that original state? You'd have to store the state even higher so it can be passed down to all components that need it. The functions would need to be passed down too. The more places you need the data the more convoluted this approach becomes.

Redux, however, creates a store which we can access, or even edit, from any component. You need to check the list in some obscure settings panel of your app? No problem just go to the store and get it. Isn't that simpler? Redux does have a fair bit of code before you can get started but, honestly, when it's all in it's easy to add new items and function to the store.

The boiler plate

Let's get all the boiler plate out the way, I make 3 folders in src each containing an index.js. These are reducers, store and actions.

reducers

This is where we create the logic behind our store. We will need a file for each store item. I want to make our list store so I'll show you the adding items logic. We'll call this file list.js

// We pass in a state, which is empty by default
// and an action which we will learn about in the 
// actions file
const listReducer = (state = {}, action) => {
  // Clone state object
  const newState = Object.assign({}, state);
  // Look for type set in the actions file
  // these types should be as unique as possible
  switch (action.type) {
    case "LISTITEM_ADD":
      // Generate random key and populate with default object.
      // Payload is set in the actions file
      newState[
        Math.random()
          .toString(36)
          .replace(/[^a-z]+/g, "")
      ] = {
        complete: false,
        label: action.payload
      };
      break;
    default:
      break;
  }

  // return the modified state
  return newState;
};

export default listReducer;
Enter fullscreen mode Exit fullscreen mode

Now let's look at the index file. The aim of the index file is to merge all of out reducers into one easy to manage reducer. Redux has a function called combineReducers for this very purpose.

import listReducer from "./list";
import { combineReducers } from "redux";

// The key of this object will be the name of the store
const rootReducers = combineReducers({ list: listReducer });

export default rootReducers;
Enter fullscreen mode Exit fullscreen mode

store

This is where the localStorage magic happens. Simply by adding these 2 functions we get to store all our data between sessions.

import { createStore } from "redux";
import rootReducers from "../reducers";

// convert object to string and store in localStorage
function saveToLocalStorage(state) {
  try {
    const serialisedState = JSON.stringify(state);
    localStorage.setItem("persistantState", serialisedState);
  } catch (e) {
    console.warn(e);
  }
}

// load string from localStarage and convert into an Object
// invalid output must be undefined
function loadFromLocalStorage() {
  try {
    const serialisedState = localStorage.getItem("persistantState");
    if (serialisedState === null) return undefined;
    return JSON.parse(serialisedState);
  } catch (e) {
    console.warn(e);
    return undefined;
  }
}

// create our store from our rootReducers and use loadFromLocalStorage
// to overwrite any values that we already have saved
const store = createStore(rootReducers, loadFromLocalStorage());

// listen for store changes and use saveToLocalStorage to
// save them to localStorage
store.subscribe(() => saveToLocalStorage(store.getState()));

export default store;
Enter fullscreen mode Exit fullscreen mode

If you don't want to store the data you will need to remove the saveToLocalStorage and loadFromLocalStorage functions also you'll need to remove loadFromLocalStorage from createStore and the whole store.subscribe line.

actions

This is where we will store our "functions", I call them function but they're super simplistic. The function simply returns an object with a type and a payload, payload is just the word we use for the parameters we pass in.

export const addItem = payload => {
  return {
    type: "LISTITEM_ADD",
    payload
  };
};
Enter fullscreen mode Exit fullscreen mode

Using provider

Provider is given to us by react-redux. It's a wrapper component we put in our React's index file. It should look a little something like this.

import React from "react";
import ReactDOM from "react-dom";
import store from "./store";
import { Provider } from "react-redux";

import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
);
Enter fullscreen mode Exit fullscreen mode

Using the store

I said at the top of this article that there was a lot of boiler plate and we're finally through it, we can finally start using the store.

useSelector

useSelector is redux-react's way of reading data from the store and it's very simple to use. You must import it and then you can use it like so.

import { useSelector } from "react-redux";

// As you see we're getting the whole state
// but we're only returning list which is the 
// name we gave it in the reducers file
const list = useSelector(state => state.list);
Enter fullscreen mode Exit fullscreen mode

We can now use list in our component as we like.

useDispatch

useDispatch is another redux-react thing. It allows you to dispatch a function to the store. Again it's quite simple to use as all the boiler plate from earlier does the heavy lifting. We need to import the function we want to use from actions and useDispatch.

import { addItem } from "../actions";
import { useDispatch } from "react-redux";

// This stores the dispatch function for using in the component
const dispatch = useDispatch();

// we run the dispatch function containing the addItem function
// As you remember addItem takes a payload and returns an object
// It will now run the reducer
dispatch(addItem(value));
Enter fullscreen mode Exit fullscreen mode

Closing thoughts

Once all the boiler plate is out of the way this makes using accessing data across components so much easier, I can see it really helping me with projects down the line. It also has the added benefit of making cross session saving super easy!

It was a bit of a long one this week but we got there. Thank you for reading. If you have any questions or corrections feel free to post them down below.

Thanks again 🦄🦄💕❤️🧡💛💚🤓🧠

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .