Repository: pmndrs/zustand
Pull request #1092 (2022-07-16 16:42:51)

style(examples): run prettier on the examples
1. Add directory `examples` and file types `js` and `jsx` to the prettier script
2. Remove deprecated config `jsxBracketSameLine`
3. Create and add `/examples/build` to `.prettierignore` because we don't want to prettier the production code
4. `yarn prettier`

codesandbox-ci[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/pmndrs/zustand/pr/1092/builds/271656) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 6b03490986fa435d7e423075e9991aaec3cfb427:

|Sandbox| Source |
|--|--|
|[React](https://codesandbox.io/s/react-s96zbv)| Configuration |
|[React Typescript](https://codesandbox.io/s/react-typescript-cm9o5z)| Configuration |
|[React Browserify](https://codesandbox.io/s/react-browserify-2pmhxr)| Configuration |
|[React Snowpack](https://codesandbox.io/s/react-snowpack-9ip4so)| Configuration |
|[React Parcel](https://codesandbox.io/s/react-parcel-bb8fse)| Configuration |
|[Next.js](https://codesandbox.io/s/next-js-q1rccn)| Configuration |


Patch:
diff --git a/docs/persisting-store-data.md b/docs/persisting-store-data.md
new file mode 100644
index 0000000..1ea4b1f
--- /dev/null
+++ b/docs/persisting-store-data.md
@@ -0,0 +1,543 @@
+# Persist middleware
+
+The persist middleware enables you to store your Zustand state in a storage (e.g. `localStorage`, `AsyncStorage`, `IndexedDB`, etc...) thus persisting it's data.
+
+Note that this middleware does support both synchronous storages (e.g. `localStorage`) and asynchronous storages (e.g. `AsyncStorage`), but using an asynchronous storage does come with a cost.
+See [Hydration and asynchronous storages](#Hydration-and-asynchronous-storages) for more details.
+
+Quick example:
+
+```ts
+import create from 'zustand'
+import { persist } from 'zustand/middleware'
+
+export const useStore = create(
+  persist(
+    (set, get) => ({
+      fishes: 0,
+      addAFish: () => set({ fishes: get().fishes + 1 }),
+    }),
+    {
+      name: 'food-storage', // name of item in the storage (must be unique)
+      getStorage: () => sessionStorage, // (optional) by default the 'localStorage' is used
+    }
+  )
+)
+```
+
+See [Options](#Options) for more details.
+
+## Options
+
+### `name`
+
+This is the only required option.
+The given name is going to be the key used to store your Zustand state in the storage, so it must be unique.
+
+### `getStorage`
+
+> Default: `() => localStorage`
+
+Enables you to use your own storage.
+Simply pass a function that returns the storage you want to use.
+
+Example:
+
+```ts
+export const useStore = create(
+  persist(
+    (set, get) => ({
+      // ...
+    }),
+    {
+      // ...
+      getStorage: () => AsyncStorage,
+    }
+  )
+)
+```
+
+The given storage must match the following interface:
+
+```ts
+interface Storage {
+  getItem: (name: string) => string | null | Promise<string | null>
+  setItem: (name: string, value: string) => void | Promise<void>
+  removeItem: (name: string) => void | Promise<void>
+}
+```
+
+### `serialize`
+
+> Schema: `(state: Object) => string | Promise<string>`
+
+> Default: `(state) => JSON.stringify(state)`
+
+Since the only way to store an object in a storage is via a string, you can use this option to give a custom function to serialize your state to a string.
+
+For example, if you want to store your state in base64:
+
+```ts
+export const useStore = create(
+  persist(
+    (set, get) => ({
+      // ...
+    }),
+    {
+      // ...
+      serialize: (state) => btoa(JSON.stringify(state)),
+    }
+  )
+)
+```
+
+Note that you would also need a custom `deserialize` function to make this work properly. See below.
+
+### `deserialize`
+
+> Schema: `(str: string) => Object | Promise<Object>`
+
+> Default: `(str) => JSON.parse(str)`
+
+If you pass a custom serialize function, you will most likely need to pass a custom deserialize function as well.
+
+To continue the example above, you could deserialize the base64 value using the following:
+
+```ts
+export const useStore = create(
+  persist(
+    (set, get) => ({
+      // ...
+    }),
+    {
+      // ...
+      deserialize: (str) => JSON.parse(atob(str)),
+    }
+  )
+)
+```
+
+### `partialize`
+
+> Schema: `(state: Object) => Object`
+
+> Default: `(state) => state`
+
+Enables you to omit some of the state's fields to be stored in the storage.
+
+You could omit multiple fields using the following:
+
+```ts
+export const useStore = create(
+  persist(
+    (set, get) => ({
+      foo: 0,
+      bar: 1,
+    }),
+    {
+      // ...
+      partialize: (state) =>
+        Object.fromEntries(
+          Object.entries(state).filter(([key]) => !['foo'].includes(key))
+        ),
+    }
+  )
+)
+```
+
+Or you could allow only specific fields using the following:
+
+```ts
+export const useStore = create(
+  persist(
+    (set, get) => ({
+      foo: 0,
+      bar: 1,
+    }),
+    {
+      // ...
+      partialize: (state) => ({ foo: state.foo }),
+    }
+  )
+)
+```
+
+### `onRehydrateStorage`
+
+> Schema: `(state: Object) => ((state?: Object, error?: Error) => void) | void`
+
+This option enables you to pass a listener function that will be called when the storage is hydrated.
+
+Example:
+
+```ts
+export const useStore = create(
+  persist(
+    (set, get) => ({
+      // ...
+    }),
+    {
+      // ...
+      onRehydrateStorage: (state) => {
+        console.log('hydration starts')
+
+        // optional
+        return (state, error) => {
+          if (error) {
+            console.log('an error happened during hydration', error)
+          } else {
+            console.log('hydration finished')
+          }
+        }
+      },
+    }
+  )
+)
+```
+
+### `version`
+
+> Schema: `number`
+
+> Default: `0`
+
+If you want to introduce a breaking change in your storage (e.g. renaming a field), you can specify a new version number.
+By default, if the version in the storage does not match the version in the code, the stored value won't be used.
+
+See the `migrate` option below for more details about handling breaking changes.
+
+### `migrate`
+
+> Schema: `(persistedState: Object, version: number) => Object | Promise<Object>`
+
+> Default: `(persistedState) => persistedState`
+
+You can use this option to handle versions migration.
+The migrate function takes the persisted state and the version number as arguments. It must return a state that is compliant to the latest version (the version in the code).
+
+For instance, if you want to rename a field, you can use the following:
+
+```ts
+export const useStore = create(
+  persist(
+    (set, get) => ({
+      newField: 0, // let's say this field was named otherwise in version 0
+    }),
+    {
+      // ...
+      version: 1, // a migration will be triggered if the version in the storage mismatches this one
+      migrate: (persistedState, version) => {
+        if (version === 0) {
+          // if the stored value is in version 0, we rename the field to the new name
+          persistedState.newField = persistedState.oldField
+          delete persistedState.oldField
+        }
+
+        return persistedState
+      },
+    }
+  )
+)
+```
+
+### `merge`
+
+> Schema: `(persistedState: Object, currentState: Object) => Object`
+
+> Default: `(persistedState, currentState) => ({ ...currentState, ...persistedState })`
+
+In some cases, you might want to use a custom merge function to merge the persisted value with the current state.
+
+By default, the middleware does a shallow merge.
+The shallow merge might not be enough if you have partially persisted nested objects.
+For instance, if the storage contains the following:
+
+```ts
+{
+  foo: {
+    bar: 0,
+  }
+}
+```
+
+But your Zustand store contains:
+
+```ts
+{
+  foo: {
+    bar: 0,
+    baz: 1,
+  }
+}
+```
+
+The shallow merge will erase the `baz` field from the `foo` object.
+One way to fix this would be to give a custom deep merge function:
+
+```ts
+export const useStore = create(
+  persist(
+    (set, get) => ({
+      foo: {
+        bar: 0,
+        baz: 1,
+      },
+    }),
+    {
+      // ...
+      merge: (persistedState, currentState) =>
+        deepMerge(currentState, persistedState),
+    }
+  )
+)
+```
+
+## API
+
+> Version: >=3.6.3
+
+The persist api enables you to do numbers of interactions with the persist middleware from inside or outside a React component.
+
+### `getOptions`
+
+> Schema: `() => Partial<PersistOptions>`
+
+This method can you get the options of the middleware.
+
+For example, it can be used to obtain the storage name:
+
+```ts
+useStore.persist.getOptions().name
+```
+
+### `setOptions`
+
+> Schema: `(newOptions: PersistOptions) => void`
+
+This method enables you to change the middleware options. Note that the new options will be merged with the current ones.
+
+For instance, this can be used to change the storage name:
+
+```ts
+useStore.persist.setOptions({
+  name: 'new-name',
+})
+```
+
+Or even to change the storage engine:
+
+```ts
+useStore.persist.setOptions({
+  getStorage: () => sessionStorage,
+})
+```
+
+### `clearStorage`
+
+> Schema: `() => void`
+
+This can be used to fully clear the persisted value in the storage.
+
+```ts
+useStore.persist.clearStorage()
+```
+
+### `rehydrate`
+
+> Schema: `() => Promise<void>`
+
+In some cases, you might want to trigger a rehydration manually.
+This can be done by calling the `rehydrate` method.
+
+```ts
+await useStore.persist.rehydrate()
+```
+
+### `hasHydrated`
+
+> Schema: `() => boolean`
+
+This is a non-reactive getter to know if the storage has been hydrated (note that this does update when calling `useStore.persist.rehydrate()`).
+
+```ts
+useStore.persist.hasHydrated()
+```
+
+### `onHydrate`
+
+> Schema: `(listener: (state) => void) => () => void`
+
+The given listener will be called when the hydration process starts.
+
+```ts
+const unsub = useStore.persist.onHydrate((state) => {
+  console.log('hydration starts')
+})
+
+// later on...
+unsub()
+```
+
+### `onFinishHydration`
+
+> Schema: `(listener: (state) => void) => () => void`
+
+The given listener will be called when the hydration process ends.
+
+```ts
+const unsub = useStore.persist.onFinishHydration((state) => {
+  console.log('hydration finished')
+})
+
+// later on...
+unsub()
+```
+
+## Hydration and asynchronous storages
+
+To explain what's the "cost" of asynchronous storages, you need to understand what's hydration.
+In a nutshell, hydration is the process of retrieving the persisted state from the storage and merging it with the current state.
+
+The persist middleware does two kinds of hydration: synchronous and asynchronous.
+If the given storage is synchronous (e.g. `localStorage`), hydration will be done synchronously. On the other hand, if the given storage is asynchronous (e.g. `AsyncStorage`), hydration will be done ... 🥁 asynchronously.
+
+But what's the catch?
+With synchronous hydration, the Zustand store will have been hydrated at its creation.
+While with asynchronous hydration, the Zustand store will be hydrated later on, in a microtask.
+
+Why does it matter?
+Asynchronous hydration can cause some unexpected behaviors.
+For instance, if you use Zustand in a React app, the store will _not_ be hydrated at the initial render. In cases where you app depends on the persisted value at page load, you might want to wait until the store has been hydrated before showing anything (e.g. your app might think the user is not logged in because it's the default, while in reality the store has not been hydrated yet).
+
+If your app does depends on the persisted state at page load, see [How can I check if my store has been hydrated?](#how-can-I-check-if-my-store-has-been-hydrated) in the Q/A.
+
+## Q/A
+
+### How can I check if my store has been hydrated?
+
+There's a fiew different ways to do this.
+
+You can use the `onRehydrateStorage` option to update a field in the store:
+
+```ts
+const useStore = create(
+  persist(
+    (set, get) => ({
+      // ...
+      _hasHydrated: false,
+      setHasHydrated: (state) => {
+        set({
+          _hasHydrated: state
+        });
+      }
+    }),
+    {
+      // ...
+      onRehydrateStorage: () => (state) => {
+        state.setHasHydrated(true)
+      }
+    }
+  )
+);
+
+export default function App() {
+  const hasHydrated = useStore(state => state._hasHydrated);
+
+  if (!hasHydrated) {
+    return <p>Loading...</p>
+  }
+
+  return (
+    // ...
+  );
+}
+```
+
+You can also create a custom `useHydration` hook:
+
+```ts
+const useStore = create(persist(...))
+
+const useHydration = () => {
+  const [hydrated, setHydrated] = useState(useStore.persist.hasHydrated)
+
+  useEffect(() => {
+    const unsubHydrate = useStore.persist.onHydrate(() => setHydrated(false)) // Note: this is just in case you want to take into account manual rehydrations. You can remove this if you don't need it/don't want it.
+    const unsubFinishHydration = useStore.persist.onFinishHydration(() => setHydrated(true))
+
+    setHydrated(useStore.persist.hasHydrated())
+
+    return () => {
+      unsubHydrate()
+      unsubFinishHydration()
+    }
+  }, [])
+
+  return hydrated
+}
+```
+
+### How can I use a custom storage engine?
+
+If the storage you want to use does not match the expected API, you can create your own storage:
+
+```ts
+import create from 'zustand'
+import { persist, StateStorage } from 'zustand/middleware'
+import { get, set, del } from 'idb-keyval' // can use anything: IndexedDB, Ionic Storage, etc.
+
+// Custom storage object
+const storage: StateStorage = {
+  getItem: async (name: string): Promise<string | null> => {
+    console.log(name, 'has been retrieved')
+    return (await get(name)) || null
+  },
+  setItem: async (name: string, value: string): Promise<void> => {
+    console.log(name, 'with value', value, 'has been saved')
+    await set(name, value)
+  },
+  removeItem: async (name: string): Promise<void> => {
+    console.log(name, 'has been deleted')
+    await del(name)
+  },
+}
+
+export const useStore = create(
+  persist(
+    (set, get) => ({
+      fishes: 0,
+      addAFish: () => set({ fishes: get().fishes + 1 }),
+    }),
+    {
+      name: 'food-storage', // unique name
+      getStorage: () => storage,
+    }
+  )
+)
+```
+
+### How can I rehydrate on storage event?
+
+You can use the `persist` api to create your own implementation, similar to what we see below
+
+```ts
+
+type StoreWithPersist = Mutate<StoreApi<State>, [["zustand/persist", unknown]]>
+
+export const withStorageDOMEvents = (store: StoreWithPersist) => {
+  const storageEventCallback = (e: StorageEvent) => {
+    if (e.key === store.persist.getOptions().name && e.newValue) {
+      store.persist.rehydrate()
+    }
+  }
+
+  window.addEventListener('storage', storageEventCallback)
+
+  return () => {
+    window.removeEventListener('storage', storageEventCallback)
+  }
+}
+
+const useStore = create(persist(...))
+withStorageDOMEvents(useStore)
+```
diff --git a/examples/package.json b/examples/package.json
index 2dcd4ae..028436f 100644
--- a/examples/package.json
+++ b/examples/package.json
@@ -39,7 +39,7 @@
     "semi": false,
     "trailingComma": "es5",
     "singleQuote": true,
-    "jsxBracketSameLine": true,
+    "bracketSameLine": true,
     "tabWidth": 2,
     "printWidth": 120
   },
diff --git a/examples/src/components/Backdrop.js b/examples/src/components/Backdrop.js
index 65056e9..84e4766 100644
--- a/examples/src/components/Backdrop.js
+++ b/examples/src/components/Backdrop.js
@@ -10,8 +10,7 @@ export default function Backdrop() {
       className="canvas-container"
       orthographic
       gl={{ powerPreference: 'high-performance', antialias: false, stencil: false, alpha: false, depth: false }}
-      camera={{ zoom: 5, position: [0, 0, 200], far: 300, near: 0 }}
-    >
+      camera={{ zoom: 5, position: [0, 0, 200], far: 300, near: 0 }}>
       <Suspense fallback={null}>
         <Scene dof={dof} />
       </Suspense>
diff --git a/examples/src/components/CopyButton.js b/examples/src/components/CopyButton.js
index 75d9604..d69cd45 100644
--- a/examples/src/components/CopyButton.js
+++ b/examples/src/components/CopyButton.js
@@ -9,7 +9,7 @@ export default function CopyButton({ code, ...props }) {
   const [isCopied, setIsCopied] = useState(false)
   const timer = useRef()
 
-  const handleCopy =  useCallback(()  => {
+  const handleCopy = useCallback(() => {
     clearTimeout(timer.current)
     copyToClipboard(code).then(() => {
       setIsCopied(true)
@@ -34,8 +34,7 @@ export default function CopyButton({ code, ...props }) {
               strokeWidth={2}
               strokeLinecap="round"
               strokeLinejoin="round"
-              {...props}
-            >
+              {...props}>
               <rect x={9} y={9} width={13} height={13} rx={2} ry={2} />
               <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
             </svg>
diff --git a/examples/src/utils/copy-to-clipboard.js b/examples/src/utils/copy-to-clipboard.js
index c4a553f..2fcaecb 100644
--- a/examples/src/utils/copy-to-clipboard.js
+++ b/examples/src/utils/copy-to-clipboard.js
@@ -1,3 +1,3 @@
 export const copyToClipboard = (str) => {
   return navigator.clipboard.writeText(str)
-}
\ No newline at end of file
+}
diff --git a/package.json b/package.json
index ca1d5d4..306be3e 100644
--- a/package.json
+++ b/package.json
@@ -68,8 +68,8 @@
     "build:shallow": "rollup -c --config-shallow",
     "build:context": "rollup -c --config-context",
     "postbuild": "yarn copy",
-    "prettier": "prettier '*.{js,json,md}' '{src,tests,docs}/**/*.{ts,tsx,md,mdx}' --write",
-    "prettier:ci": "prettier '*.{js,json,md}' '{src,tests,docs}/**/*.{ts,tsx,md,mdx}' --list-different",
+    "prettier": "prettier '*.{js,json,md}' '{examples/src,src,tests,docs}/**/*.{js,jsx,ts,tsx,md,mdx}' --write",
+    "prettier:ci": "prettier '*.{js,json,md}' '{examples/src,src,tests,docs}/**/*.{js,jsx,ts,tsx,md,mdx}' --list-different",
     "eslint": "eslint --fix '*.{js,json}' '{src,tests}/**/*.{ts,tsx}'",
     "eslint:ci": "eslint '*.{js,json}' '{src,tests}/**/*.{ts,tsx}'",
     "pretest": "tsc --noEmit",
diff --git a/src/middleware/persist.ts b/src/middleware/persist.ts
index 861eb0c..d81e629 100644
--- a/src/middleware/persist.ts
+++ b/src/middleware/persist.ts
@@ -85,6 +85,7 @@ interface StorePersist<S extends State, Ps> {
     hasHydrated: () => boolean
     onHydrate: (fn: PersistListener<S>) => () => void
     onFinishHydration: (fn: PersistListener<S>) => () => void
+    getOptions: () => Partial<PersistOptions<S, Ps>>
   }
 }
 
@@ -276,6 +277,7 @@ const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
     clearStorage: () => {
       storage?.removeItem(options.name)
     },
+    getOptions: () => options,
     rehydrate: () => hydrate() as Promise<void>,
     hasHydrated: () => hasHydrated,
     onHydrate: (cb) => {
@@ -303,7 +305,7 @@ type Persist = <
   T extends State,
   Mps extends [StoreMutatorIdentifier, unknown][] = [],
   Mcs extends [StoreMutatorIdentifier, unknown][] = [],
-  U = Partial<T>
+  U = T
 >(
   initializer: StateCreator<T, [...Mps, ['zustand/persist', unknown]], Mcs>,
   options?: PersistOptions<T, U>
diff --git a/src/shallow.ts b/src/shallow.ts
index d585373..1aaf2a5 100644
--- a/src/shallow.ts
+++ b/src/shallow.ts
@@ -1,4 +1,24 @@
-export default function shallow<T, U>(objA: T, objB: U) {
+function shallow<
+  T extends (...args: unknown[]) => unknown,
+  U extends (...args: unknown[]) => unknown
+>(objA: T, objB: U): boolean
+
+function shallow<
+  T extends string | number | boolean,
+  U extends string | number | boolean
+>(objA: T, objB: U): boolean
+
+function shallow<T extends unknown[], U extends unknown[]>(
+  objA: T,
+  objB: U
+): boolean
+
+function shallow<
+  T extends Record<string, unknown>,
+  U extends Record<string, unknown>
+>(objA: T, objB: U): boolean
+
+function shallow<T, U>(objA: T, objB: U) {
   if (Object.is(objA, objB)) {
     return true
   }
@@ -24,3 +44,5 @@ export default function shallow<T, U>(objA: T, objB: U) {
   }
   return true
 }
+
+export default shallow
diff --git a/src/vanilla.ts b/src/vanilla.ts
index f28a034..0ccfc14 100644
--- a/src/vanilla.ts
+++ b/src/vanilla.ts
@@ -15,7 +15,7 @@ export interface Subscribe<T extends State> {
 
 export type SetState<T extends State> = {
   _(
-    partial: T | Partial<T> | ((state: T) => T | Partial<T>),
+    partial: T | Partial<T> | { _(state: T): T | Partial<T> }['_'],
     replace?: boolean | undefined
   ): void
 }['_']


Test patch:
diff --git a/tests/persistSync.test.tsx b/tests/persistSync.test.tsx
index 798c987..aebe09e 100644
--- a/tests/persistSync.test.tsx
+++ b/tests/persistSync.test.tsx
@@ -381,6 +381,23 @@ describe('persist middleware with sync configuration', () => {
     )
   })
 
+  it('can access the options through the api', () => {
+    const storage = {
+      getItem: () => null,
+      setItem: jest.fn(),
+      removeItem: () => {},
+    }
+
+    const useStore = create(
+      persist(() => ({ count: 0 }), {
+        name: 'test-storage',
+        getStorage: () => storage,
+      })
+    )
+    expect(useStore.persist.getOptions().name).toBeDefined()
+    expect(useStore.persist.getOptions().name).toBe('test-storage')
+  })
+
   it('can change the options through the api', () => {
     const setItemSpy = jest.fn()
 
@@ -394,6 +411,7 @@ describe('persist middleware with sync configuration', () => {
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
         getStorage: () => storage,
+        partialize: (s) => s as Partial<typeof s>,
       })
     )