Repository: reduxjs/redux-toolkit
Pull request #623 (2020-06-18 23:12:04)

Add section to usage guide on working with non-serializable data
Adds
* Section to usage guide on working with non-serializable data
* Updates error message thrown when encountering non-serializable values to include a link to the new section

netlify[bot]: Deploy preview for *redux-starter-kit-docs* ready!

Built with commit 572995700d0d4137b6e30c6931b40d0cdde2857e

https://deploy-preview-623--redux-starter-kit-docs.netlify.app
cloeper: So, there's a number of failing tests on master. About 10 tests fail, all look like this:

```
TypeScript diagnostics (customize using `[jest-config].globals.ts-jest.diagnostics` option):
src/configureStore.ts:178:5 - error TS2345: Argument of type 'DeepPartial<S>' is not assignable to parameter of type 'PreloadedState<S>'.
```

My changes have nothing to do with that, so I'm unsure how to proceed.
markerikson: Hmm. Looking at build failures listed in Travis, they specifically seem to be about the snapshots for the serializability middleware. Not immediately seeing any TS-related issues.
markerikson: Also, why is that "fixes" commit full of formatting changes? Are we flip-flopping Prettier versions between people's installs or something?
cloeper: > Also, why is that "fixes" commit full of formatting changes? Are we flip-flopping Prettier versions between people's installs or something?

I wish I could tell you. I've run `yarn format` so I would assume that would resolve any issue.
cloeper: Also, I updated the snapshots, but I have no idea why the tests aren't passing in travis. The only tests that don't pass for me locally are the ones I mentioned earlier.

 This repo just isn't behaving as I would expect it to and I don't have the time to figure it out. Feel free to take the changes from my commits and adding them in a different PR.
phryneas: There was a rogue space. I pushed a fix, let's see if it works now.
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/reduxjs/redux-toolkit/pr/623/builds/33467) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 7755ffbe4d5db680aa872765c7745b603efee74b:

|Sandbox| Source |
|--|--|
|[proud-smoke-1xfhu](https://codesandbox.io/s/proud-smoke-1xfhu)| Configuration |
|[delicate-framework-x3ic6](https://codesandbox.io/s/delicate-framework-x3ic6)| Configuration |
|[hardcore-elion-qm6ct](https://codesandbox.io/s/hardcore-elion-qm6ct)| Configuration |

markerikson: I think we at least ought to have a specific example of setting up with Redux-Persist, because A) it's a legit example of customizing the behavior, and B) it's come up fairly frequently in the issues.
msutkowski: @markerikson There for sure is with `react-redux-firebase` as they add Timestamp instances into the store and other `instance` types in almost every action. I've made a few contributions to that and `redux-firestore` and plan on opening a PR for their 4.0 release to address the serializable as all of that can be handled rather simply by calling `instance.toString()` or `instance.toJSON()` depending on the firebase instance type.

Either way, an example of the configuration needed for those libs looks like this:

```ts
import {
    configureStore,
    getDefaultMiddleware,
  } from "@reduxjs/toolkit";
  import {
    getFirebase,
    actionTypes as rrfActionTypes,
  } from "react-redux-firebase";
  import { constants as rfConstants } from "redux-firestore";
  import rootReducer from "./rootReducer";
  import { useDispatch } from "react-redux";

  const extraArgument = {
    getFirebase,
  };
  export type DefaultThunkExtra = typeof extraArgument;
  
  const middleware = [
    ...getDefaultMiddleware({
        serializableCheck: {
          ignoredActions: [
              // just ignore every redux-firebase and react-redux-firebase action type
            ...Object.keys(rfConstants.actionTypes).map(
              (type) => `${rfConstants.actionsPrefix}/${type}`
            ),
            ...Object.keys(rrfActionTypes).map((type) => `@@reactReduxFirebase/${type}`),
          ],
          ignoredPaths: ["firebase", "firestore"],
        },
        thunk: {
          extraArgument,
        },
      }),
  ] as const;
  
  const store = configureStore({
    reducer: rootReducer,
    middleware,
  });

  export type AppDispatch = typeof store.dispatch;
  export const useAppDispatch = () => useDispatch<AppDispatch>();
  
  export default store;
  ```

_*Note: there is more typing that needs to be done specifically for the reducers they ship to be TS compatible with RTK, but is probably a separate issue. I haven't seen many people mention that, but a PR I had opened for the problem is hopefully getting merged in their 3.6 release*_

Looking at that, we should probably add another property of `ignoreActionPrefix` for these scenarios _(or some other partial matcher)_. I believe `redux-websocket` still has this same type of issue, although a more limited # of action types. I'm sure there's even more that I'm not aware of 🤷 


Patch:
diff --git a/docs/usage/usage-guide.md b/docs/usage/usage-guide.md
index 86ce1d83..15a76ad4 100644
--- a/docs/usage/usage-guide.md
+++ b/docs/usage/usage-guide.md
@@ -1021,3 +1021,18 @@ export const usersAdapter = createEntityAdapter({
   sortComparer: (a, b) => a.first_name.localeCompare(b.first_name)
 })
 ```
+
+## Working with Non-serializable Data
+
+Occasionally, you may have actions that need to accept non-serializable data. This should be done very rarely and only if necessary. These non-serializable payloads shouldn't ever make it into your application state through a reducer. However, if you're using libraries such as redux saga or you're using middleware that could reasonably use non-serializable data you'll want to do the following:
+
+```js
+configureStore({
+  //...
+  middleware: getDefaultMiddleware({
+    serializableCheck: {
+      ignoredActions: ['your/action']
+    }
+  })
+})
+```
diff --git a/src/entities/fixtures/book.ts b/src/entities/fixtures/book.ts
index 6191374a..8c0a8d5d 100644
--- a/src/entities/fixtures/book.ts
+++ b/src/entities/fixtures/book.ts
@@ -1,6 +1,7 @@
 export interface BookModel {
   id: string
   title: string
+  author?: string
 }
 
 export const AClockworkOrange: BookModel = Object.freeze({
@@ -17,3 +18,9 @@ export const TheGreatGatsby: BookModel = Object.freeze({
   id: 'tgg',
   title: 'The Great Gatsby'
 })
+
+export const TheHobbit: BookModel = Object.freeze({
+  id: 'th',
+  title: 'The Hobbit',
+  author: 'J. R. R. Tolkien'
+})
diff --git a/src/entities/unsorted_state_adapter.ts b/src/entities/unsorted_state_adapter.ts
index 13f20733..4fec5c1f 100644
--- a/src/entities/unsorted_state_adapter.ts
+++ b/src/entities/unsorted_state_adapter.ts
@@ -16,7 +16,7 @@ export function createUnsortedStateAdapter<T>(
 ): EntityStateAdapter<T> {
   type R = EntityState<T>
 
-  function addOneMutably(entity: T, state: EntityState<T>): void {
+  function addOneMutably(entity: T, state: R): void {
     const key = selectIdValue(entity, selectId)
 
     if (key in state.entities) {
@@ -108,10 +108,15 @@ export function createUnsortedStateAdapter<T>(
       if (update.id in state.entities) {
         // If there are multiple updates to one entity, merge them together
         updatesPerEntity[update.id] = {
+          id: update.id,
           // Spreads ignore falsy values, so this works even if there isn't
           // an existing update already at this key
-          ...updatesPerEntity[update.id],
-          ...update
+          changes: {
+            ...(updatesPerEntity[update.id]
+              ? updatesPerEntity[update.id].changes
+              : null),
+            ...update.changes
+          }
         }
       }
     })
diff --git a/src/serializableStateInvariantMiddleware.ts b/src/serializableStateInvariantMiddleware.ts
index a69aaad2..03bda311 100644
--- a/src/serializableStateInvariantMiddleware.ts
+++ b/src/serializableStateInvariantMiddleware.ts
@@ -176,7 +176,8 @@ export function createSerializableStateInvariantMiddleware(
           value,
           '\nTake a look at the logic that dispatched this action: ',
           action,
-          '\n(See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)'
+          '\n(See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)',
+          '\n(To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)'
         )
       }
     })


Test patch:
diff --git a/src/entities/unsorted_state_adapter.test.ts b/src/entities/unsorted_state_adapter.test.ts
index 44f5d696..3e4521b5 100644
--- a/src/entities/unsorted_state_adapter.test.ts
+++ b/src/entities/unsorted_state_adapter.test.ts
@@ -4,7 +4,8 @@ import {
   BookModel,
   TheGreatGatsby,
   AClockworkOrange,
-  AnimalFarm
+  AnimalFarm,
+  TheHobbit
 } from './fixtures/book'
 import { createNextState } from '..'
 
@@ -466,12 +467,20 @@ describe('Unsorted State Adapter', () => {
     test('updateMany', () => {
       const firstChange = { title: 'First Change' }
       const secondChange = { title: 'Second Change' }
-      const withMany = adapter.setAll(state, [TheGreatGatsby, AClockworkOrange])
+      const thirdChange = { title: 'Third Change' }
+      const fourthChange = { author: 'Fourth Change' }
+      const withMany = adapter.setAll(state, [
+        TheGreatGatsby,
+        AClockworkOrange,
+        TheHobbit
+      ])
 
       const result = createNextState(withMany, draft => {
         adapter.updateMany(draft, [
-          { id: TheGreatGatsby.id, changes: firstChange },
-          { id: AClockworkOrange.id, changes: secondChange }
+          { id: TheHobbit.id, changes: firstChange },
+          { id: TheGreatGatsby.id, changes: secondChange },
+          { id: AClockworkOrange.id, changes: thirdChange },
+          { id: TheHobbit.id, changes: fourthChange }
         ])
       })
 
@@ -480,16 +489,22 @@ describe('Unsorted State Adapter', () => {
           "entities": Object {
             "aco": Object {
               "id": "aco",
-              "title": "Second Change",
+              "title": "Third Change",
             },
             "tgg": Object {
               "id": "tgg",
+              "title": "Second Change",
+            },
+            "th": Object {
+              "author": "Fourth Change",
+              "id": "th",
               "title": "First Change",
             },
           },
           "ids": Array [
             "tgg",
             "aco",
+            "th",
           ],
         }
       `)
diff --git a/src/serializableStateInvariantMiddleware.test.ts b/src/serializableStateInvariantMiddleware.test.ts
index a512ed50..6e769776 100644
--- a/src/serializableStateInvariantMiddleware.test.ts
+++ b/src/serializableStateInvariantMiddleware.test.ts
@@ -111,7 +111,8 @@ describe('serializableStateInvariantMiddleware', () => {
       Take a look at the logic that dispatched this action:  Object {
         \\"type\\": Symbol(SOME_CONSTANT),
       } 
-      (See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)"
+      (See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants) 
+      (To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)"
     `)
   })
 
@@ -363,7 +364,8 @@ describe('serializableStateInvariantMiddleware', () => {
           },
           \\"type\\": \\"test\\",
         } 
-        (See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)"
+        (See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants) 
+        (To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)"
       `)
     })