Skip to content

In my previous article about frontend state persistence, I mentioned I'd explain the vuex-persistedstate plugin — after all, I shouldn't leave that promise unfulfilled. Here I'll cover the more important parts of the vuex-persistedstate source code. Let's start with the full source:

Source Code

typescript
import { Store, MutationPayload } from "vuex";
import merge from "deepmerge";
import * as shvl from "shvl";

interface Storage {
  getItem: (key: string) => any;
  setItem: (key: string, value: any) => void;
  removeItem: (key: string) => void;
}

interface Options {
  key?: string;
  paths?: string[];
  reducer?: (state: any, paths: string[]) => object;
  subscriber?: (
    store: typeof Store
  ) => (handler: (mutation: any, state: any) => void) => void;
  storage?: Storage;
  getState?: (key: string, storage: Storage) => any;
  setState?: (key: string, state: typeof Store, storage: Storage) => void;
  filter?: (mutation: MutationPayload) => boolean;
  arrayMerger?: (state: any, saved: any) => any;
  rehydrated?: (store: typeof Store) => void;
  fetchBeforeUse?: boolean;
  overwrite?: boolean;
  assertStorage?: (storage: Storage) => void | Error;
}

export default function(options?: Options) {
  options = options || {};

  const storage = options.storage || (window && window.localStorage);
  const key = options.key || "vuex";

  function getState(key, storage) {
    let value;

    try {
      return (value = storage.getItem(key)) && typeof value !== "undefined"
        ? JSON.parse(value)
        : undefined;
    } catch (err) {}

    return undefined;
  }

  function filter() {
    return true;
  }

  function setState(key, state, storage) {
    return storage.setItem(key, JSON.stringify(state));
  }

  function reducer(state, paths) {
    return Array.isArray(paths)
      ? paths.reduce(function(substate, path) {
          return shvl.set(substate, path, shvl.get(state, path));
        }, {})
      : state;
  }

  function subscriber(store) {
    return function(handler) {
      return store.subscribe(handler);
    };
  }

  const assertStorage =
    options.assertStorage ||
    (() => {
      storage.setItem("@@", 1);
      storage.removeItem("@@");
    });

  assertStorage(storage);

  const fetchSavedState = () => (options.getState || getState)(key, storage);

  let savedState;

  if (options.fetchBeforeUse) {
    savedState = fetchSavedState();
  }

  return function(store) {
    if (!options.fetchBeforeUse) {
      savedState = fetchSavedState();
    }

    if (typeof savedState === "object" && savedState !== null) {
      store.replaceState(
        options.overwrite
          ? savedState
          : merge(store.state, savedState, {
              arrayMerge:
                options.arrayMerger ||
                function(store, saved) {
                  return saved;
                },
              clone: false,
            })
      );
      (options.rehydrated || function() {})(store);
    }

    (options.subscriber || subscriber)(store)(function(mutation, state) {
      if ((options.filter || filter)(mutation)) {
        (options.setState || setState)(
          key,
          (options.reducer || reducer)(state, options.paths),
          storage
        );
      }
    });
  };
}

If this already makes sense to you, great. If not, let's go through it step by step:

Interface Parameter Fields Explained with Source Code

typescript
// Defines methods for local storage operations
interface Storage {
  getItem: (key: string) => any;
  setItem: (key: string, value: any) => void;
  removeItem: (key: string) => void;
}

// Configuration options
interface Options {
  key?: string;
  paths?: string[];
  reducer?: (state: any, paths: string[]) => object;
  subscriber?: (
    store: typeof Store
  ) => (handler: (mutation: any, state: any) => void) => void;
  storage?: Storage;
  getState?: (key: string, storage: Storage) => any;
  setState?: (key: string, state: typeof Store, storage: Storage) => void;
  filter?: (mutation: MutationPayload) => boolean;
  arrayMerger?: (state: any, saved: any) => any;
  rehydrated?: (store: typeof Store) => void;
  fetchBeforeUse?: boolean;
  overwrite?: boolean;
  assertStorage?: (storage: Storage) => void | Error;
}

Let's analyze the most important parameter fields in the Options interface:

key

key is the name used to store data in storage, defaulting to "vuex". If you don't specify a Storage type, vuex-persistedstate defaults to localStorage, equivalent to localStorage.setItem('vuex', data).

paths & reducer

These two fields work together. paths is an array, and reducer is a function. You can pass just paths or just reducer to specify which values to persist. Looking at this source code snippet makes their purpose clear:

typescript
function reducer(state, paths) {
  return Array.isArray(paths)
    ? paths.reduce(function(substate, path) {
        return shvl.set(substate, path, shvl.get(state, path));
      }, {})
    : state;
}

subscriber, storage, getState, setState, filter

These five fields should be discussed together. storage defines the methods used to talk to storage. The plugin works with any storage object that implements the Storage interface; by default it uses localStorage, while sessionStorage and cookies require you to pass your own adapter. getState and setState define how values are read from and written to storage. subscriber sets up the Vuex mutation subscription, defaulting to store.subscribe(handler). filter accepts a function whose parameter is a Vuex mutation — when this function returns false, setState is not triggered. These parameters work together, as shown in the source:

typescript
// subscriber definition
function subscriber(store) {
  return function(handler) {
    return store.subscribe(handler);
  };
}

// filter definition
function filter() {
  return true;
}

(options.subscriber || subscriber)(store)(function(mutation, state) {
  if ((options.filter || filter)(mutation)) {
    (options.setState || setState)(
      key,
      (options.reducer || reducer)(state, options.paths),
      storage
    );
  }
});

From the code above, subscriber returns a function after the first call, which is defined as:

typescript
subscriber?: (
    store: typeof Store
  ) => (handler: (mutation: any, state: any) => void) => void;

The default filter simply returns true, so unless you supply your own filter the state is written to storage on every mutation. To skip saving a particular mutation, return false from your custom filter. The subscriber callback receives (mutation, state) — the same signature filter's parameter uses.

Summary

This library is great for TypeScript beginners to learn from — the logic isn't too simple or too complex, and it helps solidify TypeScript knowledge. And with that, that gap is filled.