pmndrs / zustand

🐻 Bear necessities for state management in React

Home Page:https://zustand-demo.pmnd.rs/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Web Storage API & Safari

nothingrandom opened this issue · comments

Summary

Using the Web Storage API for persisting storage (specifically for browser extensions), @andyhails and I discovered that there is a bug in createJSONStorage for Safari that causes it to stringify and persist store functions (e.g. foo()) as objects (e.g. foo: {}).

This does not exist as an issue for Firefox, Chrome, or other Chromium browsers.

We found this bug to be extremely inconsistent between builds for Safari through xCode and throughout runtime, however, did find a fix through a custom createJSONStorage.

Proposal

Filter functions out from being in the persisted store. Zustand handles this just fine, combining the persisted store of strings, booleans, and objects with the non-persisted store which includes those functions no longer being persisted.

createJSONStorage.ts

import { PersistStorage, StateStorage, StorageValue } from 'zustand/middleware';
import { StoreState } from '@/store/index';

/**
 * createJSONStorage - our own version of zustand's createLocalStorage middleware.
 *
 * There is a bug in zustand's createLocalStorage middleware for Safari that causes it to stringify and
 * persist StateStore functions (e.g. foo()) as objects (e.g. foo: {}).
 *
 * Overriding the createJSONStorage middleware with our own version that filters out functions from the state
 * mitigates this issue.
 */

/**
 * Parses a string into a StorageValue.
 * @param str
 */
const parse = (str: string | null): StorageValue<StoreState> => (str ? JSON.parse(str) : null);

/**
 * Filters out properties that cannot be persisted to storage (e.g. functions).
 * This is usually handled by the browser well but for some reason in Safari there are side effects.
 *
 * @param state
 */
const statePropertiesOnly = (state: StoreState) =>
  Object.fromEntries(
    Object.entries(state)
      .filter(([_, value]) => typeof value !== 'function'),
  );

export default (getStorage: () => StateStorage): PersistStorage<StoreState> => ({
  getItem: name => {
    const item = getStorage().getItem(name);
    return item instanceof Promise ? item.then(parse) : parse(item);
  },
  setItem: (name, newValue: StorageValue<StoreState>) =>
    getStorage().setItem(name, JSON.stringify(statePropertiesOnly(newValue.state))),
  removeItem: name => getStorage().removeItem(name),
});