Persisted Data With Redux and LocalStorage — The Proper Way 2024

Using react at some scale of data flow we’ll find ourself using 3rd party state management like redux (mobx, Recoil are other examples). However those state managements are not persisted, once the client (user) refresh the page all data is gone forever and

Archiecture perspective

When to use persisted browser data?

There are few cases where it makes sense to use persisted data, and there are cons and pros to each case.

  • We don’t have or want backend — it could be because we want lean frontend application only, it could be because getting a database will significantly increase our expenses.
  • We don’t need to share data across users — for whatever the reason the product doesn’t require to save the data for long term, to manage the data, observe the data through a dashboard or the product doesn’t even care if there is data or not.
  • We want to save the data for the long term but we don’t want to save EVERY step to make a change in the app. Meaning that we will save the data to a DB but once in a while and till then we’ll want the data to be persisted for the user.
  • It’s ok for data to be lost — for product reasoning, it’s ok for some data to lost, or forgotten. Maybe the user writes a draft and we want to keep it but we don’t want to waste our money and resources storing that draft.

What’s LocalStorage

Local storage is a way to persist data to the user browser, here are the pros and cons of local storage.

Pros

  1. It’s client side — it’s all on the user end
  2. It’s easy to use — very simple API of saving data and pulling data
  3. It doesn’t cost us resources.
  4. It’s offline ! a very big pro since we don’t need the user to be online to save data, and we can send that data to our DB, once the user is back online.

Cons

  1. It’s client side — which means we have less control over this data.
  2. No secured — because we store it in the browser, it’s not the safest place to store data, someone else can steal that data and access the data and we wouldn’t even know about it.
  3. Prone to mistakes — if you save data in wrong manner, or make a change to data structure you might break things without even noticing till it’s too late.
  4. Anyone can inject whatever data they want — not much filtering can be done, it’s harder to get strict structure of the data.

How to use localStorage with Redux

First we’ll start with Dan’s Abramov localstorage utility functions.

//localStorage.js
export const loadState = () => {
   try {
     const serializedState = localStorage.getItem('state');
     if (serializedState === null) {
       return undefined;
     }
     return JSON.parse(serializedState);
   } catch (err) {
     return undefined;
   }
 };
  export const saveState = (state) => {
   try {
     const serializedState = JSON.stringify(state);
     localStorage.setItem('state', serializedState);
   } catch (err) {
       console.error(err);
   }
 };

In the above example we have 2 simple loadState and saveState functions. What they do is to use native localStorage api (setItem and getItem) with the help of JSON API to save and get the data.

However we need to know where to stitch those 3rd party api to our 3rd party state management in a proper way.

// store.js
import { configureStore } from "@reduxjs/toolkit";
import scoreBoardSlice from '../ScoreBoard/scoreBoardSlice'; // ignore
import { loadState, saveState } from './localStorage'; // <-- our utility of localstorage
import { debounce } from './index'; // <-- our utility to prevent writing too often.
const persistedState = loadState();
 
const store = configureStore({
 reducer: scoreBoardSlice.reducer,
 preloadedState: persistedState, // <--- where we pre-load data we saved to local storeage
});
 
 
store.subscribe(debounce(() => {
 saveState({
   scoreBoard: store.getState().scoreBoard, // <--- where we save data to localstorage  once every 1000ms 
 });
}, 1000));
 
 
export {
 store
};

In the above example taken from my Candy Crush mock game. We create the redux store, this is where we want to manage all of our persisted data. We don’t want it inside reducers, actions or render methods. We want it to be at the highest level of our application architecture. Store.js is essentially where we boot our application with initial data, and in this case we also create a subscribe so we can also save data that comes from the user interaction with the application.

Performance issues

One of the most important things is to avoid writing too often to the localstorage, we don’t want to write every time the user is engaged with the UI. so we’ll be using a debounce (we can also use lodash debounce methods) or write one of our own, here’s our code:

// debounce.js
export const debounce = (fnc, delay) => {
   let time;
 
   return function toBeExecuted(...args) {
       const later = () => {
           clearTimeout(time);
           fnc(...args);
       };
 
       clearTimeout(time);
       time = setTimeout(later, delay);
   };
};

Bottom line

At larger scale you might going to need to break down writing to the localStorage, you might want to be picky what to write what not to write to the local storage (for example don’t save sensitive information like passwords or emails) and you also need to know your limitation with localStorage and in some cases be notify about it as an error when user reach those limitation. For instance, according to the HTML5 spec (which introduces us to the localStorage API), The limitation of data storage is 5MB per application per browser. So in that case we either need to make sure data is not too big, or we might want to cut down on older data and remove it or we might want to restructure our data in some way or encrypt our data.

Leave a Reply

Your email address will not be published. Required fields are marked *

All rights reserved 2024 ©