Repository: pmndrs/zustand
Pull request #1451 (2022-11-29 04:09:33)

feat(shallow): Add support of Maps and Sets to shallow comparator
## Summary
Zustand allows storing Maps and Sets, however, the `shallow` comparator returns incorrect comparison results for these type.
This PR adds support for shallow comparison of ES6 `Map` and `Set`


## Check List

- [x] `yarn run prettier` for formatting code and docs


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/1451/builds/320599) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit e37c1dfbf2d1b65a2f48275dfa6f22054b4bcdcf:

|Sandbox| Source |
|--|--|
|[React](https://codesandbox.io/s/react-c8krj3)| Configuration |
|[React Typescript](https://codesandbox.io/s/react-typescript-gj4qol)| Configuration |
|[React Browserify](https://codesandbox.io/s/react-browserify-3nhnq4)| Configuration |
|[React Snowpack](https://codesandbox.io/s/react-snowpack-xrqfz1)| Configuration |
|[React Parcel](https://codesandbox.io/s/react-parcel-p7xsl1)| Configuration |
|[Next.js](https://codesandbox.io/s/next-js-fs8xu8)| Configuration |

dai-shi: See some discussions in #1064.
I'm not sure if we should keep modifying `shallow` or simply drop it and suggest alternatives.
But, indeed we have maps and sets usage in docs, so this should be good to go.

Do anyone have any insights?

Patch:
diff --git a/docs/guides/auto-generating-selectors.md b/docs/guides/auto-generating-selectors.md
index 27f5320..fe30662 100644
--- a/docs/guides/auto-generating-selectors.md
+++ b/docs/guides/auto-generating-selectors.md
@@ -14,13 +14,13 @@ However, writing these could be tedious. If that is the case for you, you can au
 ## create the following function: `createSelectors`
 
 ```typescript
-import { State, StoreApi, UseBoundStore } from 'zustand'
+import { StoreApi, UseBoundStore } from 'zustand'
 
 type WithSelectors<S> = S extends { getState: () => infer T }
   ? S & { use: { [K in keyof T]: () => T[K] } }
   : never
 
-const createSelectors = <S extends UseBoundStore<StoreApi<State>>>(
+const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(
   _store: S
 ) => {
   let store = _store as WithSelectors<typeof _store>
diff --git a/docs/integrations/3rd-party-libraries.md b/docs/integrations/3rd-party-libraries.md
index 589437f..e9a3565 100644
--- a/docs/integrations/3rd-party-libraries.md
+++ b/docs/integrations/3rd-party-libraries.md
@@ -11,11 +11,13 @@ Zustand provides bear necessities for state management which is great for most p
 - [@dhmk/zustand-lens](https://github.com/dhmk083/dhmk-zustand-lens) - Lens support for zustand
 - [@liveblocks/zustand](https://github.com/liveblocks/liveblocks/tree/main/packages/liveblocks-zustand) - Liveblocks middleware to make your application multiplayer.
 - [auto-zustand-selectors-hook](https://github.com/Albert-Gao/auto-zustand-selectors-hook) - Auto generate zustand hooks for you store with Typescript support
+- [derive-zustand](https://github.com/dai-shi/derive-zustand) - A function to create a derived Zustand store from stores
 - [geschichte](https://github.com/BowlingX/geschichte) - zustand and immer based hook to manage query parameters
 - [mobz](https://github.com/2A5F/Mobz) - zustand style mobx api
 - [shared-zustand](https://github.com/Tom-Julux/shared-zustand) - cross-tab state sharing for zustand
 - [simple-zustand-devtools](https://github.com/beerose/simple-zustand-devtools) - Inspect your zustand store in React DevTools 🐻⚛️
 - [solid-zustand](https://github.com/wobsoriano/solid-zustand) - Zustand state management for Solid.
+- [use-zustand](https://github.com/dai-shi/use-zustand) - Another custom hook to use Zustand vanilla store
 - [vue-zustand](https://github.com/wobsoriano/vue-zustand) - State-management solution for Vue based on Zustand.
 - [zoov](https://github.com/InfiniteXyy/zoov) - Use 🐻 Zustand with Module-like api
 - [zundo](https://github.com/charkour/zundo) - 🍜 enable time-travel in your apps. undo/redo middleware for zustand
diff --git a/docs/integrations/updating-draft-states.md b/docs/integrations/updating-draft-states.md
index 4130645..6ca475d 100644
--- a/docs/integrations/updating-draft-states.md
+++ b/docs/integrations/updating-draft-states.md
@@ -111,7 +111,7 @@ If you are using `Immer`, make sure you are actually following the rules of
 
 For example, you have to add `[immerable] = true` for
 [class objects](https://immerjs.github.io/immer/complex-objects) to work. If
-you don't to this, `Immer` will still mutate the object, but not as a proxy, so
+you don't do this, `Immer` will still mutate the object, but not as a proxy, so
 it will also update the current state. `Zustand` checks if the state has
 actually changed, so since both the current state as well as the next state are
 equal (if you don't do it correctly), it will skip calling the subscriptions.
diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts
index 1c6641d..6f958e5 100644
--- a/src/middleware/devtools.ts
+++ b/src/middleware/devtools.ts
@@ -160,7 +160,7 @@ const devtoolsImpl: DevtoolsImpl =
     try {
       extensionConnector =
         (enabled ?? __DEV__) && window.__REDUX_DEVTOOLS_EXTENSION__
-    } catch {
+    } catch (e) {
       // ignored
     }
 
diff --git a/src/shallow.ts b/src/shallow.ts
index 6d9ec7c..0e7f59e 100644
--- a/src/shallow.ts
+++ b/src/shallow.ts
@@ -10,6 +10,29 @@ function shallow<T>(objA: T, objB: T) {
   ) {
     return false
   }
+
+  if (objA instanceof Map && objB instanceof Map) {
+    if (objA.size !== objB.size) return false
+
+    for (const [key, value] of objA) {
+      if (!Object.is(value, objB.get(key))) {
+        return false
+      }
+    }
+    return true
+  }
+
+  if (objA instanceof Set && objB instanceof Set) {
+    if (objA.size !== objB.size) return false
+
+    for (const value of objA) {
+      if (!objB.has(value)) {
+        return false
+      }
+    }
+    return true
+  }
+
   const keysA = Object.keys(objA)
   if (keysA.length !== Object.keys(objB).length) {
     return false


Test patch:
diff --git a/tests/shallow.test.tsx b/tests/shallow.test.tsx
index 681b217..a60008e 100644
--- a/tests/shallow.test.tsx
+++ b/tests/shallow.test.tsx
@@ -39,6 +39,43 @@ describe('shallow', () => {
     expect(shallow([{ foo: 'bar' }], [{ foo: 'bar', asd: 123 }])).toBe(false)
   })
 
+  it('compares Maps', () => {
+    function createMap<T extends object>(obj: T) {
+      return new Map(Object.entries(obj))
+    }
+
+    expect(
+      shallow(
+        createMap({ foo: 'bar', asd: 123 }),
+        createMap({ foo: 'bar', asd: 123 })
+      )
+    ).toBe(true)
+
+    expect(
+      shallow(
+        createMap({ foo: 'bar', asd: 123 }),
+        createMap({ foo: 'bar', foobar: true })
+      )
+    ).toBe(false)
+
+    expect(
+      shallow(
+        createMap({ foo: 'bar', asd: 123 }),
+        createMap({ foo: 'bar', asd: 123, foobar: true })
+      )
+    ).toBe(false)
+  })
+
+  it('compares Sets', () => {
+    expect(shallow(new Set(['bar', 123]), new Set(['bar', 123]))).toBe(true)
+
+    expect(shallow(new Set(['bar', 123]), new Set(['bar', 2]))).toBe(false)
+
+    expect(shallow(new Set(['bar', 123]), new Set(['bar', 123, true]))).toBe(
+      false
+    )
+  })
+
   it('compares functions', () => {
     function firstFnCompare() {
       return { foo: 'bar' }