Repository: reduxjs/redux-toolkit
Pull request #442 (2020-03-24 18:38:46)

Add basic createEntityAdapter usage docs
Addresses #441 

- Adds basic usage for `createEntityAdapter`
- Example usage for basic `createEntityAdapter` [CodeSandbox](https://codesandbox.io/s/rtk-entities-basic-example-4jg0m) _(currently linked in usage guide)_
- Combined normalizr/createEntityAdapter shown in this [CodeSandbox](https://codesandbox.io/s/rtk-entities-basic-example-with-normalizr-j30td)

Pending changes from #444:
- [x] Make a brief note of this in the "Usage with TS" page
- [ ] Create CodeSandbox to reference `normalizr` usage example

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

Latest deployment of this branch, based on commit 1761797e34311bc44c8173517ceabce79d00147b:

|Sandbox| Source |
|--|--|
|[pensive-bas-unoiy](https://codesandbox.io/s/pensive-bas-unoiy)| Configuration |
|[romantic-mestorf-2oy1x](https://codesandbox.io/s/romantic-mestorf-2oy1x)| Configuration |
|[trusting-gagarin-p4dfl](https://codesandbox.io/s/trusting-gagarin-p4dfl)| Configuration |

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

Latest deployment of this branch, based on commit e88773e871f9147c88e2e5610761a9f8008fe5e8:

|Sandbox| Source |
|--|--|
|[confident-moon-yf94o](https://codesandbox.io/s/confident-moon-yf94o)| Configuration |
|[proud-bash-cnyc9](https://codesandbox.io/s/proud-bash-cnyc9)| Configuration |
|[affectionate-zhukovsky-5g5rp](https://codesandbox.io/s/affectionate-zhukovsky-5g5rp)| Configuration |

markerikson: Don't know why there's no PR docs preview going.  Thought that should still show up for draft PRs.
markerikson: Let's just make this a normal PR and see if the previews kick in.
markerikson: Grr.  It did, sort of, but the Netlify settings are conflicting between the `netlify.toml` file and the settings in the dashboard, and it's failing somehow.
markerikson: @msutkowski , @phryneas : y'know, I don't see any reason why we couldn't update the "many" methods to accept a `Dictionary<Entity>` in addition to an `Entity[]`.

Yeah, the overloads get a bit larger, but that seems like a reasonable thing to add. 
msutkowski: I updated this CodeSandbox to reflect the same thing as shown in the usage docs: https://codesandbox.io/s/rtk-entities-basic-example-with-normalizr-j30td

Patch:
diff --git a/docs/api/createEntityAdapter.md b/docs/api/createEntityAdapter.md
index e13e7de8..9d5d4d5c 100644
--- a/docs/api/createEntityAdapter.md
+++ b/docs/api/createEntityAdapter.md
@@ -196,14 +196,14 @@ export interface EntityAdapter<T> extends EntityStateAdapter<T> {
 The primary content of an entity adapter is a set of generated reducer functions for adding, updating, and removing entity instances from an entity state object:
 
 - `addOne`: accepts a single entity, and adds it
-- `addMany`: accepts an array of entities, and adds them
-- `setAll`: accepts an array of entities, and replaces the existing entity contents with the values in the array
+- `addMany`: accepts an array of entities or an object in the shape of `Record<EntityId, T>`, and adds them.
+- `setAll`: accepts an array of entities or an object in the shape of `Record<EntityId, T>`, and replaces the existing entity contents with the values in the array
 - `removeOne`: accepts a single entity ID value, and removes the entity with that ID if it exists
 - `removeMany`: accepts an array of entity ID values, and removes each entity with those IDs if they exist
 - `updateOne`: accepts an "update object" containing an entity ID and an object containing one or more new field values to update inside a `changes` field, and updates the corresponding entity
 - `updateMany`: accepts an array of update objects, and updates all corresponding entities
 - `upsertOne`: accepts a single entity. If an entity with that ID exists, the fields in the update will be merged into the existing entity, with any matching fields overwriting the existing values. If the entity does not exist, it will be added.
-- `upsertMany`: accepts an array of entities that will be upserted.
+- `upsertMany`: accepts an array of entities or an object in the shape of `Record<EntityId, T>` that will be upserted.
 
 Each method has a signature that looks like:
 
diff --git a/docs/usage/usage-guide.md b/docs/usage/usage-guide.md
index ff451d03..900fd504 100644
--- a/docs/usage/usage-guide.md
+++ b/docs/usage/usage-guide.md
@@ -700,3 +700,313 @@ interface ThunkAPI {
 ```
 
 You can use any of these as needed inside the payload callback to determine what the final result should be.
+
+## Managing Normalized Data
+
+Most applications typically deal with data that is deeply nested or relational. The goal of normalizing data is to efficiently organize the data in your state. This is typically done by storing collections as objects with the key of an `id`, while storing a sorted array of those `ids`. For a more in-depth explanation and further examples, there is a great reference in the [Redux docs page on "Normalizing State Shape"](https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape).
+
+### Normalizing by hand
+
+Normalizing data doesn't require any special libraries. Here's a basic example of how you might normalize the response from a `fetchAll` API request that returns data in the shape of `{ users: [{id: 1, first_name: 'normalized', last_name: 'person'}] }`, using some hand-written logic:
+
+```js
+import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
+import userAPI from './userAPI'
+
+export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
+  const response = await userAPI.fetchAll()
+  return response.data
+})
+
+export const slice = createSlice({
+  name: 'users',
+  initialState: {
+    ids: [],
+    entities: {}
+  },
+  reducers: {},
+  extraReducers: builder => {
+    builder.addCase(fetchUsers.fulfilled, (state, action) => {
+      // reduce the collection by the id property into a shape of { 1: { ...user }}
+      const byId = action.payload.users.reduce((byId, user) => {
+        byId[user.id] = user
+        return byId
+      }, {})
+      state.entities = byId
+      state.ids = Object.keys(byId)
+    })
+  }
+})
+```
+
+Although we're capable of writing this code, it does become repetitive, especially if you're handling multiple types of data. In addition, this example only handles loading entries into the state, not updating them.
+
+### Normalizing with `normalizr`
+
+[`normalizr`](https://github.com/paularmstrong/normalizr) is a popular existing library for normalizing data. You can use it on its own without Redux, but it is very commonly used with Redux. The typical usage is to format collections from an API response and then process them in your reducers.
+
+```js
+import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
+import { normalize, schema } from 'normalizr'
+
+import userAPI from './userAPI'
+
+const userEntity = new schema.Entity('users')
+
+export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
+  const response = await userAPI.fetchAll()
+  // Normalize the data before passing it to our reducer
+  const normalized = normalize(response.data, [userEntity])
+  return normalized.entities
+})
+
+export const slice = createSlice({
+  name: 'users',
+  initialState: {
+    ids: [],
+    entities: {}
+  },
+  reducers: {},
+  extraReducers: builder => {
+    builder.addCase(fetchUsers.fulfilled, (state, action) => {
+      state.entities = action.payload.users
+      state.ids = Object.keys(action.payload.users)
+    })
+  }
+})
+```
+
+As with the hand-written version, this doesn't handle adding additional entries into the state, or updating them later - it's just loading in everything that was received.
+
+### Normalizing with `createEntityAdapter`
+
+Redux Toolkit's `createEntityAdapter` API provides a standardized way to store your data in a slice by taking a collection and putting it into the shape of `{ ids: [], entities: {} }`. Along with this predefined state shape, it generates a set of reducer functions and selectors that know how to work with the data.
+
+```js
+import {
+  createSlice,
+  createAsyncThunk,
+  createEntityAdapter
+} from '@reduxjs/toolkit'
+import userAPI from './userAPI'
+
+export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
+  const response = await userAPI.fetchAll()
+  // In this case, `response.data` would be:
+  //  [{id: 1, first_name: 'Example', last_name: 'User'}]
+  return response.data
+})
+
+export const usersAdapter = createEntityAdapter()
+
+// By default, `createEntityAdapter` gives you `{ ids: [], entities: {} }`.
+// If you want to track 'loading' or other keys, you would initialize them here:
+// `getInitialState({ loading: false, activeRequestId: null })`
+const initialState = usersAdapter.getInitialState()
+
+export const slice = createSlice({
+  name: 'users',
+  initialState,
+  reducers: {
+    removeUser: usersAdapter.removeOne
+  },
+  extraReducers: builder => {
+    builder.addCase(fetchUsers.fulfilled, usersAdapter.upsertMany)
+  }
+})
+
+const reducer = slice.reducer
+export default reducer
+
+export const { removeUser } = slice.actions
+```
+
+You can [view the full code of this example usage on CodeSandbox](https://codesandbox.io/s/rtk-entities-basic-example-1xubt)
+
+### Using `createEntityAdapter` with Normalization Libraries
+
+If you're already using `normalizr` or another normalization library, you could consider using it along with `createEntityAdapter`. To expand on the examples above, here is a demonstration of how we could use `normalizr` to format a payload, then leverage the utilities `createEntityAdapter` provides.
+
+By default, the `setAll`, `addMany`, and `upsertMany` CRUD methods expect an array of entities. However, they also allow you to pass in an object that is in the shape of `{ 1: { id: 1, ... }}` as an alternative, which makes it easier to insert pre-normalized data.
+
+```js
+// features/articles/articlesSlice.js
+import {
+  createSlice,
+  createEntityAdapter,
+  createAsyncThunk,
+  createSelector
+} from '@reduxjs/toolkit'
+import fakeAPI from '../../services/fakeAPI'
+import { normalize, schema } from 'normalizr'
+
+// Define normalizr entity schemas
+export const userEntity = new schema.Entity('users')
+export const commentEntity = new schema.Entity('comments', {
+  commenter: userEntity
+})
+export const articleEntity = new schema.Entity('articles', {
+  author: userEntity,
+  comments: [commentEntity]
+})
+
+const articlesAdapter = createEntityAdapter()
+
+export const fetchArticle = createAsyncThunk(
+  'articles/fetchArticle',
+  async id => {
+    const data = await fakeAPI.articles.show(id)
+    // Normalize the data so reducers can load a predictable payload, like:
+    // `action.payload = { users: {}, articles: {}, comments: {} }`
+    const normalized = normalize(data, articleEntity)
+    return normalized.entities
+  }
+)
+
+export const slice = createSlice({
+  name: 'articles',
+  initialState: articlesAdapter.getInitialState(),
+  reducers: {},
+  extraReducers: {
+    [fetchArticle.fulfilled]: (state, action) => {
+      // Handle the fetch result by inserting the articles here
+      articlesAdapter.upsertMany(state, action.payload.articles)
+    }
+  }
+})
+
+const reducer = slice.reducer
+export default reducer
+
+// features/users/usersSlice.js
+
+import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'
+import { fetchArticle } from '../articles/articlesSlice'
+
+const usersAdapter = createEntityAdapter()
+
+export const slice = createSlice({
+  name: 'users',
+  initialState: usersAdapter.getInitialState(),
+  reducers: {},
+  extraReducers: builder => {
+    builder.addCase(fetchArticle.fulfilled, (state, action) => {
+      // And handle the same fetch result by inserting the users here
+      usersAdapter.upsertMany(state, action.payload.users)
+    })
+  }
+})
+
+const reducer = slice.reducer
+export default reducer
+
+// features/comments/commentsSlice.js
+
+import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'
+import { fetchArticle } from '../articles/articlesSlice'
+
+const commentsAdapter = createEntityAdapter()
+
+export const slice = createSlice({
+  name: 'comments',
+  initialState: commentsAdapter.getInitialState(),
+  reducers: {},
+  extraReducers: {
+    [fetchArticle.fulfilled]: (state, action) => {
+      // Same for the comments
+      commentsAdapter.upsertMany(state, action.payload.comments)
+    }
+  }
+})
+
+const reducer = slice.reducer
+export default reducer
+```
+
+You can [view the full code of this example `normalizr` usage on CodeSandbox](https://codesandbox.io/s/rtk-entities-basic-example-with-normalizr-bm3ie)
+
+### Using selectors with `createEntityAdapter`
+
+The entity adapter providers a selector factory that generates the most common selectors for you. Taking the examples above, we can add selectors to our `usersSlice` like this:
+
+```js
+// Rename the exports for readability in component usage
+export const {
+  selectById: selectUserById,
+  selectIds: selectUserIds,
+  selectEntities: selectUserEntities,
+  selectAll: selectAllUsers,
+  selectTotal: selectTotalUsers
+} = usersAdapter.getSelectors(state => state.users)
+```
+
+You could then use these selectors in a component like this:
+
+```js
+import React from 'react'
+import { useSelector } from 'react-redux'
+import { selectTotalUsers, selectAllUsers } from './usersSlice'
+
+import styles from './UsersList.module.css'
+
+export function UsersList() {
+  const count = useSelector(selectTotalUsers)
+  const users = useSelector(selectAllUsers)
+
+  return (
+    <div>
+      <div className={styles.row}>
+        There are <span className={styles.value}>{count}</span> users.{' '}
+        {count === 0 && `Why don't you fetch some more?`}
+      </div>
+      {users.map(user => (
+        <div key={user.id}>
+          <div>{`${user.first_name} ${user.last_name}`}</div>
+        </div>
+      ))}
+    </div>
+  )
+}
+```
+
+### Specifying Alternate ID Fields
+
+By default, `createEntityAdapter` assumes that your data has unique IDs in an `entity.id` field. If your data set stores its ID in a different field, you can pass in a `selectId` argument that returns the appropriate field.
+
+```js
+// In this instance, our user data always has a primary key of `idx`
+const userData = {
+  users: [
+    { idx: 1, first_name: 'Test' },
+    { idx: 2, first_name: 'Two' }
+  ]
+}
+
+// Since our primary key is `idx` and not `id`,
+// pass in an ID selector to return that field instead
+export const usersAdapter = createEntityAdapter({
+  selectId: user => user.idx
+})
+```
+
+### Sorting Entities
+
+`createEntityAdapter` provides a `sortComparer` argument that you can leverage to sort the collection of `ids` in state. This can be very useful for when you want to guarantee a sort order and your data doesn't come presorted.
+
+```js
+// In this instance, our user data always has a primary key of `idx`
+const userData = {
+  users: [
+    { id: 1, first_name: 'Test' },
+    { id: 2, first_name: 'Banana' }
+  ]
+}
+
+// Sort by `first_name`. `state.ids` would be ordered as
+// `ids: [ 2, 1 ]`, since 'B' comes before 'T'.
+// When using the provided `selectAll` selector, the result would be sorted:
+// [{ id: 2, first_name: 'Banana' }, { id: 1, first_name: 'Test' }]
+export const usersAdapter = createEntityAdapter({
+  sortComparer: (a, b) => a.first_name.localeCompare(b.first_name)
+})
+```
diff --git a/docs/usage/usage-with-typescript.md b/docs/usage/usage-with-typescript.md
index 0ae3a309..276b9764 100644
--- a/docs/usage/usage-with-typescript.md
+++ b/docs/usage/usage-with-typescript.md
@@ -569,3 +569,58 @@ const booksSlice = createSlice({
   }
 })
 ```
+
+### Using `createEntityAdapter` with `normalizr`
+
+When using a library like [`normalizr`](https://github.com/paularmstrong/normalizr/), your normalized data will resemble this shape:
+
+```js
+{
+  result: 1,
+  entities: {
+    1: { id: 1, other: 'property' },
+    2: { id: 2, other: 'property' }
+  }
+}
+```
+
+The methods `addMany`, `upsertMany`, and `setAll` all allow you to pass in the `entities` portion of this directly with no extra conversion steps. However, the `normalizr` TS typings currently do not correctly reflect that multiple data types may be included in the results, so you will need to specify that type structure yourself.
+
+Here is an example of how that would look:
+
+```ts
+type Author = { id: number; name: string }
+type Article = { id: number; title: string }
+type Comment = { id: number; commenter: number }
+
+export const fetchArticle = createAsyncThunk(
+  'articles/fetchArticle',
+  async (id: number) => {
+    const data = await fakeAPI.articles.show(id)
+    // Normalize the data so reducers can responded to a predictable payload.
+    // Note: at the time of writing, normalizr does not automatically infer the result,
+    // so we explicitly declare the shape of the returned normalized data as a generic arg.
+    const normalized = normalize<
+      any,
+      {
+        articles: { [key: string]: Article }
+        users: { [key: string]: Author }
+        comments: { [key: string]: Comment }
+      }
+    >(data, articleEntity)
+    return normalized.entities
+  }
+)
+
+export const slice = createSlice({
+  name: 'articles',
+  initialState: articlesAdapter.getInitialState(),
+  reducers: {},
+  extraReducers: builder => {
+    builder.addCase(fetchArticle.fulfilled, (state, action) => {
+      // The type signature on action.payload matches what we passed into the generic for `normalize`, allowing us to access specific properties on `payload.articles` if desired
+      articlesAdapter.upsertMany(state, action.payload.articles)
+    })
+  }
+})
+```
diff --git a/netlify.toml b/netlify.toml
index 8751d9b6..bf291bbb 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -2,6 +2,7 @@
   base    = "website"
   publish = "build"
   command = "npm run build"
+  ignore = "git diff --quiet HEAD^ HEAD docs website"
 
 [build.environment]
 NODE_VERSION = "10"
\ No newline at end of file
diff --git a/src/entities/models.ts b/src/entities/models.ts
index 4a569617..b5fbb651 100644
--- a/src/entities/models.ts
+++ b/src/entities/models.ts
@@ -57,16 +57,22 @@ export interface EntityStateAdapter<T> {
     action: PayloadAction<T>
   ): S
 
-  addMany<S extends EntityState<T>>(state: PreventAny<S, T>, entities: T[]): S
   addMany<S extends EntityState<T>>(
     state: PreventAny<S, T>,
-    entities: PayloadAction<T[]>
+    entities: T[] | Record<EntityId, T>
+  ): S
+  addMany<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    entities: PayloadAction<T[] | Record<EntityId, T>>
   ): S
 
-  setAll<S extends EntityState<T>>(state: PreventAny<S, T>, entities: T[]): S
   setAll<S extends EntityState<T>>(
     state: PreventAny<S, T>,
-    entities: PayloadAction<T[]>
+    entities: T[] | Record<EntityId, T>
+  ): S
+  setAll<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    entities: PayloadAction<T[] | Record<EntityId, T>>
   ): S
 
   removeOne<S extends EntityState<T>>(state: PreventAny<S, T>, key: EntityId): S
@@ -112,11 +118,11 @@ export interface EntityStateAdapter<T> {
 
   upsertMany<S extends EntityState<T>>(
     state: PreventAny<S, T>,
-    entities: T[]
+    entities: T[] | Record<EntityId, T>
   ): S
   upsertMany<S extends EntityState<T>>(
     state: PreventAny<S, T>,
-    entities: PayloadAction<T[]>
+    entities: PayloadAction<T[] | Record<EntityId, T>>
   ): S
 }
 
diff --git a/src/entities/sorted_state_adapter.ts b/src/entities/sorted_state_adapter.ts
index 93c6e5e7..4315308a 100644
--- a/src/entities/sorted_state_adapter.ts
+++ b/src/entities/sorted_state_adapter.ts
@@ -3,7 +3,8 @@ import {
   IdSelector,
   Comparer,
   EntityStateAdapter,
-  Update
+  Update,
+  EntityId
 } from './models'
 import { createStateOperator } from './state_adapter'
 import { createUnsortedStateAdapter } from './unsorted_state_adapter'
@@ -23,7 +24,14 @@ export function createSortedStateAdapter<T>(
     return addManyMutably([entity], state)
   }
 
-  function addManyMutably(newModels: T[], state: R): void {
+  function addManyMutably(
+    newModels: T[] | Record<EntityId, T>,
+    state: R
+  ): void {
+    if (!Array.isArray(newModels)) {
+      newModels = Object.values(newModels)
+    }
+
     const models = newModels.filter(
       model => !(selectIdValue(model, selectId) in state.entities)
     )
@@ -33,7 +41,10 @@ export function createSortedStateAdapter<T>(
     }
   }
 
-  function setAllMutably(models: T[], state: R): void {
+  function setAllMutably(models: T[] | Record<EntityId, T>, state: R): void {
+    if (!Array.isArray(models)) {
+      models = Object.values(models)
+    }
     state.entities = {}
     state.ids = []
 
@@ -74,7 +85,14 @@ export function createSortedStateAdapter<T>(
     return upsertManyMutably([entity], state)
   }
 
-  function upsertManyMutably(entities: T[], state: R): void {
+  function upsertManyMutably(
+    entities: T[] | Record<EntityId, T>,
+    state: R
+  ): void {
+    if (!Array.isArray(entities)) {
+      entities = Object.values(entities)
+    }
+
     const added: T[] = []
     const updated: Update<T>[] = []
 
diff --git a/src/entities/unsorted_state_adapter.ts b/src/entities/unsorted_state_adapter.ts
index 7386d9a8..8d41f242 100644
--- a/src/entities/unsorted_state_adapter.ts
+++ b/src/entities/unsorted_state_adapter.ts
@@ -24,13 +24,21 @@ export function createUnsortedStateAdapter<T>(
     state.entities[key] = entity
   }
 
-  function addManyMutably(entities: T[], state: R): void {
+  function addManyMutably(entities: T[] | Record<EntityId, T>, state: R): void {
+    if (!Array.isArray(entities)) {
+      entities = Object.values(entities)
+    }
+
     for (const entity of entities) {
       addOneMutably(entity, state)
     }
   }
 
-  function setAllMutably(entities: T[], state: R): void {
+  function setAllMutably(entities: T[] | Record<EntityId, T>, state: R): void {
+    if (!Array.isArray(entities)) {
+      entities = Object.values(entities)
+    }
+
     state.ids = []
     state.entities = {}
 
@@ -123,7 +131,14 @@ export function createUnsortedStateAdapter<T>(
     return upsertManyMutably([entity], state)
   }
 
-  function upsertManyMutably(entities: T[], state: R): void {
+  function upsertManyMutably(
+    entities: T[] | Record<EntityId, T>,
+    state: R
+  ): void {
+    if (!Array.isArray(entities)) {
+      entities = Object.values(entities)
+    }
+
     const added: T[] = []
     const updated: Update<T>[] = []
 


Test patch:
diff --git a/src/entities/sorted_state_adapter.test.ts b/src/entities/sorted_state_adapter.test.ts
index 8b45a788..104375a3 100644
--- a/src/entities/sorted_state_adapter.test.ts
+++ b/src/entities/sorted_state_adapter.test.ts
@@ -85,6 +85,24 @@ describe('Sorted State Adapter', () => {
     })
   })
 
+  it('should let you add many entities to the state from a dictionary', () => {
+    const withOneEntity = adapter.addOne(state, TheGreatGatsby)
+
+    const withManyMore = adapter.addMany(withOneEntity, {
+      [AClockworkOrange.id]: AClockworkOrange,
+      [AnimalFarm.id]: AnimalFarm
+    })
+
+    expect(withManyMore).toEqual({
+      ids: [AClockworkOrange.id, AnimalFarm.id, TheGreatGatsby.id],
+      entities: {
+        [TheGreatGatsby.id]: TheGreatGatsby,
+        [AClockworkOrange.id]: AClockworkOrange,
+        [AnimalFarm.id]: AnimalFarm
+      }
+    })
+  })
+
   it('should remove existing and add new ones on setAll', () => {
     const withOneEntity = adapter.addOne(state, TheGreatGatsby)
 
@@ -102,6 +120,23 @@ describe('Sorted State Adapter', () => {
     })
   })
 
+  it('should remove existing and add new ones on setAll when passing in a dictionary', () => {
+    const withOneEntity = adapter.addOne(state, TheGreatGatsby)
+
+    const withAll = adapter.setAll(withOneEntity, {
+      [AClockworkOrange.id]: AClockworkOrange,
+      [AnimalFarm.id]: AnimalFarm
+    })
+
+    expect(withAll).toEqual({
+      ids: [AClockworkOrange.id, AnimalFarm.id],
+      entities: {
+        [AClockworkOrange.id]: AClockworkOrange,
+        [AnimalFarm.id]: AnimalFarm
+      }
+    })
+  })
+
   it('should remove existing and add new ones on addAll (deprecated)', () => {
     const withOneEntity = adapter.addOne(state, TheGreatGatsby)
 
@@ -380,4 +415,25 @@ describe('Sorted State Adapter', () => {
       }
     })
   })
+
+  it('should let you upsert many entities in the state when passing in a dictionary', () => {
+    const firstChange = { title: 'Zack' }
+    const withMany = adapter.setAll(state, [TheGreatGatsby])
+
+    const withUpserts = adapter.upsertMany(withMany, {
+      [TheGreatGatsby.id]: { ...TheGreatGatsby, ...firstChange },
+      [AClockworkOrange.id]: AClockworkOrange
+    })
+
+    expect(withUpserts).toEqual({
+      ids: [AClockworkOrange.id, TheGreatGatsby.id],
+      entities: {
+        [TheGreatGatsby.id]: {
+          ...TheGreatGatsby,
+          ...firstChange
+        },
+        [AClockworkOrange.id]: AClockworkOrange
+      }
+    })
+  })
 })
diff --git a/src/entities/unsorted_state_adapter.test.ts b/src/entities/unsorted_state_adapter.test.ts
index c4448337..87a4ad62 100644
--- a/src/entities/unsorted_state_adapter.test.ts
+++ b/src/entities/unsorted_state_adapter.test.ts
@@ -69,6 +69,24 @@ describe('Unsorted State Adapter', () => {
     })
   })
 
+  it('should let you add many entities to the state from a dictionary', () => {
+    const withOneEntity = adapter.addOne(state, TheGreatGatsby)
+
+    const withManyMore = adapter.addMany(withOneEntity, {
+      [AClockworkOrange.id]: AClockworkOrange,
+      [AnimalFarm.id]: AnimalFarm
+    })
+
+    expect(withManyMore).toEqual({
+      ids: [TheGreatGatsby.id, AClockworkOrange.id, AnimalFarm.id],
+      entities: {
+        [TheGreatGatsby.id]: TheGreatGatsby,
+        [AClockworkOrange.id]: AClockworkOrange,
+        [AnimalFarm.id]: AnimalFarm
+      }
+    })
+  })
+
   it('should remove existing and add new ones on setAll', () => {
     const withOneEntity = adapter.addOne(state, TheGreatGatsby)
 
@@ -86,6 +104,23 @@ describe('Unsorted State Adapter', () => {
     })
   })
 
+  it('should remove existing and add new ones on setAll when passing in a dictionary', () => {
+    const withOneEntity = adapter.addOne(state, TheGreatGatsby)
+
+    const withAll = adapter.setAll(withOneEntity, {
+      [AClockworkOrange.id]: AClockworkOrange,
+      [AnimalFarm.id]: AnimalFarm
+    })
+
+    expect(withAll).toEqual({
+      ids: [AClockworkOrange.id, AnimalFarm.id],
+      entities: {
+        [AClockworkOrange.id]: AClockworkOrange,
+        [AnimalFarm.id]: AnimalFarm
+      }
+    })
+  })
+
   it('should let you add remove an entity from the state', () => {
     const withOneEntity = adapter.addOne(state, TheGreatGatsby)
 
@@ -230,14 +265,11 @@ describe('Unsorted State Adapter', () => {
 
     /*
       Original code failed with a mish-mash of values, like:
-
       {
         ids: [ 'c' ],
         entities: { b: { id: 'b', title: 'First' }, c: { id: 'c' } }
       }
-
       We now expect that only 'c' will be left:
-
       { 
         ids: [ 'c' ], 
         entities: { c: { id: 'c', title: 'First' } } 
@@ -299,4 +331,25 @@ describe('Unsorted State Adapter', () => {
       }
     })
   })
+
+  it('should let you upsert many entities in the state when passing in a dictionary', () => {
+    const firstChange = { title: 'Zack' }
+    const withMany = adapter.setAll(state, [TheGreatGatsby])
+
+    const withUpserts = adapter.upsertMany(withMany, {
+      [TheGreatGatsby.id]: { ...TheGreatGatsby, ...firstChange },
+      [AClockworkOrange.id]: AClockworkOrange
+    })
+
+    expect(withUpserts).toEqual({
+      ids: [TheGreatGatsby.id, AClockworkOrange.id],
+      entities: {
+        [TheGreatGatsby.id]: {
+          ...TheGreatGatsby,
+          ...firstChange
+        },
+        [AClockworkOrange.id]: AClockworkOrange
+      }
+    })
+  })
 })
diff --git a/type-tests/files/createEntityAdapter.typetest.ts b/type-tests/files/createEntityAdapter.typetest.ts
index c53a29ff..5e00b95e 100644
--- a/type-tests/files/createEntityAdapter.typetest.ts
+++ b/type-tests/files/createEntityAdapter.typetest.ts
@@ -41,8 +41,12 @@ function extractReducers<T>(
   })
 
   expectType<ActionCreatorWithPayload<Entity>>(slice.actions.addOne)
-  expectType<ActionCreatorWithPayload<Entity[]>>(slice.actions.addMany)
-  expectType<ActionCreatorWithPayload<Entity[]>>(slice.actions.setAll)
+  expectType<ActionCreatorWithPayload<Entity[] | Record<string, Entity>>>(
+    slice.actions.addMany
+  )
+  expectType<ActionCreatorWithPayload<Entity[] | Record<string, Entity>>>(
+    slice.actions.setAll
+  )
   expectType<ActionCreatorWithPayload<EntityId>>(slice.actions.removeOne)
   expectType<ActionCreatorWithPayload<EntityId[]>>(slice.actions.removeMany)
   expectType<ActionCreatorWithoutPayload>(slice.actions.removeAll)
@@ -51,7 +55,9 @@ function extractReducers<T>(
     slice.actions.updateMany
   )
   expectType<ActionCreatorWithPayload<Entity>>(slice.actions.upsertOne)
-  expectType<ActionCreatorWithPayload<Entity[]>>(slice.actions.upsertMany)
+  expectType<ActionCreatorWithPayload<Entity[] | Record<string, Entity>>>(
+    slice.actions.upsertMany
+  )
 }
 
 /**