Repository: pmndrs/zustand
Pull request #720 (2021-12-21 22:58:16)

devtools: minor fixes for pause recording feature
- When setting the state from the devtools we used to temporary turn off recording and then we turn it back. But turning it back isn't correct, what if it was already turned off? Hence we now restore instead of turning it on.
- If the action received isn't a string then instead of parsing it we log a "Unsupported action format"
- We're now overwriting dispatch before subscribing to the devtools just in case of hypothetical sync (eg while testing) dispatching of actions from the devtools

codesandbox[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/720/builds/202402) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 0a2fa52ecf4e7f9be4f3c9db4e5d0cb771130e77:

|Sandbox| Source |
|--|--|
|[React](https://codesandbox.io/s/react-gi8pg)| Configuration |
|[React Typescript](https://codesandbox.io/s/react-typescript-0uobk)| Configuration |
|[React Browserify](https://codesandbox.io/s/react-browserify-ixd7f)| Configuration |
|[React Snowpack](https://codesandbox.io/s/react-snowpack-pr8mi)| Configuration |
|[React Parcel](https://codesandbox.io/s/react-parcel-sfxee)| Configuration |
|[Next.js](https://codesandbox.io/s/next-js-iqt6g)| Configuration |

dai-shi: could you write some description for a record?
devanshj: Sure, done.

Patch:
diff --git a/readme.md b/readme.md
index 455cb35..7b97a9d 100644
--- a/readme.md
+++ b/readme.md
@@ -440,10 +440,32 @@ const useStore = create(devtools(redux(reducer, initialState)))
 devtools takes the store function as its first argument, optionally you can name the store or configure [serialize](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md#serialize) options with a second argument.  
   
 Name store: `devtools(store, {name: "MyStore"})`, which will create a seperate instance named "MyStore" in the devtools.
-Serialize options: `devtools(store, { serialize: { options: true } })`.  
+
+Serialize options: `devtools(store, { serialize: { options: true } })`.
   
+#### Logging Actions
+
 devtools will only log actions from each separated store unlike in a typical *combined reducers* redux store. See an approach to combining stores https://github.com/pmndrs/zustand/issues/163
 
+You can log a specific action type for each `set` function by passing a third parameter:
+
+```jsx
+const createBearSlice = (set, get) => ({
+  eatFish: () =>
+    set(
+      (prev) => ({ fishes: prev.fishes > 1 ? prev.fishes - 1 : 0 }),
+      false,
+      "bear/eatFish"
+    ),
+})
+```
+
+If an action type is not provided, it is defaulted to "anonymous". You can customize this default value by providing an `anonymousActionType` parameter: 
+
+```jsx
+devtools(..., { anonymousActionType: 'unknown', ... })
+```
+
 ## React context
 
 The store created with `create` doesn't require context providers. In some cases, you may want to use contexts for dependency injection or if you want to initialize your store with props from a component. Because the store is a hook, passing it as a normal context value may violate rules of hooks. To avoid misusage, a special `createContext` is provided.
diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts
index bd69eac..e1fa957 100644
--- a/src/middleware/devtools.ts
+++ b/src/middleware/devtools.ts
@@ -182,17 +182,39 @@ export const devtools =
       )
     }
     const setStateFromDevtools: SetState<S> = (...a) => {
+      const originalIsRecording = isRecording
       isRecording = false
       set(...a)
-      isRecording = true
+      isRecording = originalIsRecording
     }
 
     const initialState = fn(api.setState, get, api)
     extension.init(initialState)
 
+    if (api.dispatchFromDevtools && typeof api.dispatch === 'function') {
+      let didWarnAboutReservedActionType = false
+      const originalDispatch = api.dispatch
+      api.dispatch = (...a: any[]) => {
+        if (a[0].type === '__setState' && !didWarnAboutReservedActionType) {
+          console.warn(
+            '[zustand devtools middleware] "__setState" action type is reserved ' +
+              'to set state from the devtools. Avoid using it.'
+          )
+          didWarnAboutReservedActionType = true
+        }
+        ;(originalDispatch as any)(...a)
+      }
+    }
+
     extension.subscribe((message: any) => {
       switch (message.type) {
         case 'ACTION':
+          if (typeof message.payload !== 'string') {
+            console.error(
+              '[zustand devtools middleware] Unsupported action format'
+            )
+            return
+          }
           return parseJsonThen<{ type: unknown; state?: PartialState<S> }>(
             message.payload,
             (action) => {
@@ -245,21 +267,6 @@ export const devtools =
       }
     })
 
-    if (api.dispatchFromDevtools && typeof api.dispatch === 'function') {
-      let didWarnAboutReservedActionType = false
-      const originalDispatch = api.dispatch
-      api.dispatch = (...a: any[]) => {
-        if (a[0].type === '__setState' && !didWarnAboutReservedActionType) {
-          console.warn(
-            '[zustand devtools middleware] "__setState" action type is reserved ' +
-              'to set state from the devtools. Avoid using it.'
-          )
-          didWarnAboutReservedActionType = true
-        }
-        ;(originalDispatch as any)(...a)
-      }
-    }
-
     return initialState
   }
 


Test patch:
diff --git a/tests/devtools.test.tsx b/tests/devtools.test.tsx
index 4509f16..99424d1 100644
--- a/tests/devtools.test.tsx
+++ b/tests/devtools.test.tsx
@@ -187,14 +187,7 @@ describe('when it receives an message of type...', () => {
       }).not.toThrow()
 
       expect(console.error).toHaveBeenLastCalledWith(
-        '[zustand devtools middleware] Could not parse the received json',
-        (() => {
-          try {
-            JSON.parse({ name: 'increment', args: [] } as unknown as string)
-          } catch (e) {
-            return e
-          }
-        })()
+        '[zustand devtools middleware] Unsupported action format'
       )
 
       expect(api.getState()).toBe(initialState)
@@ -489,3 +482,18 @@ it('works in non-browser env', () => {
 
   global.window = originalWindow
 })
+
+it('preserves isRecording after setting from devtools', () => {
+  const api = create(devtools(() => ({ count: 0 })))
+  ;(extensionSubscriber as (message: any) => void)({
+    type: 'DISPATCH',
+    payload: { type: 'PAUSE_RECORDING' },
+  })
+  ;(extensionSubscriber as (message: any) => void)({
+    type: 'ACTION',
+    payload: '{ "type": "__setState", "state": { "foo": "bar" } }',
+  })
+
+  api.setState({ count: 1 })
+  expect(extension.send).not.toBeCalled()
+})