DEV Community

Rakan Nimer
Rakan Nimer

Posted on • Updated on

Add Offline-Caching to Your React Reducer with 5 Lines of Code

When building web applications I often use this method to add local storage caching of my state.

Suppose we're starting with :


const [state, dispatch] = React.useReducer(
  reducer,
  initialState
)

as a first step we create a method that takes in a reducer and outputs another one, a higher-order function:

const withLocalStorageCache = reducer => { return (state, action) => {
  const newState = reducer(state, action);
  localStorage.setItem("my-state", JSON.stringify(newState));
  return newState;
}};

and we wrap our reducer with it before passing the result to the useReducer hook.


const [state, dispatch] = React.useReducer(
  withCache(reducer),
  initialState
)


And finally we replace initialState with the latest cached version of it :

const [state, dispatch] = React.useReducer(
  withCache(reducer),
  localStorage.getItem("my-state")
)

And that' s it !

One line of code modified and 5 new ones and our whole state is synced with localStorage.

We can, of course, limit our caching to only specific parts of the state instead of the whole state object. And limit our updates to the cache by action or state. But we'll leave that as an exercise to the user.

localStorage is supported by 97.62% of browsers.

Complete demo

Discussion (5)

Collapse
rakannimer profile image
Rakan Nimer Author • Edited on

What happens when more than one reducer stores data under the my-state key?

Very weird stuff ! It's not at all recommended to do that.

What happens when one wants to upgrade the structure of of the store between deployments?

It's recommended to change the state key when changing the structure of the store (e.g. my-state-1, my-state-2)

What happens when the store contains a Map or an instantiated class or a function?

This technique as is, requires the state to be serializable, to handle special cases you would need to add custom parsing and stringifying for non-serializable variables or simply ignore them when reading and writing to the the cache.

rakannimer profile image
Rakan Nimer Author • Edited on

Thanks for taking the time to write that !

The main reason I didn't go into it, is to keep the code under 5 lines 😅

I wanted the reader to quickly understand the idea of using HOFs with the reducer and build on it and experiment themselves.

Now they can also read/use this hook 👌

Thread Thread
bionicles profile image
bion howard • Edited on
// src/state/hook.jsx

import React, { createContext, useContext, useReducer, useMemo } from "react";
import { reduce, logger, initialState } from "state";

const LOGGING = 1;

export const StateContext = createContext();

export const withCache = reducer => (s, a) => {
  const newS = reducer(s, a);
  localStorage.setItem("state", JSON.stringify(newS));
  return newS;
};

export const StateProvider = ({ children }) => {
  const [state, dispatch] = useReducer(
    withCache(LOGGING ? logger : reduce),
    JSON.parse(localStorage.getItem("state")) || initialState
  );
  const value = useMemo(() => {
    return { state, dispatch };
  }, [state, dispatch]);
  return (
    <StateContext.Provider value={value}>{children}</StateContext.Provider>
  );
};

export const useState = () => useContext(StateContext);
// src/app.js
import { StateProvider } from "state";

... layout stuff ...

const App = () => (
  <StateProvider>
    <Layout />
  </StateProvider>
);

export default App;
// src/components/my-component.jsx
import React from "react";
import { useState } from "state";

const MyComponent = props => {
    const { state: { thingy }, dispatch } = useState();
    return // my jsx
}
// src/state/index.js
export * from "state/initial-state";
export * from "state/reduce";
export * from "state/logger";
export * from "state/hook";