Skip to main content

Entity Adapter

When you have lists of items (like users, posts, todos), Entity Adapter makes it easy to manage them. It gives you helpful functions to add, update, and remove items.

Basic Setup​

import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'

// Create adapter
const usersAdapter = createEntityAdapter()

// Get initial state
const initialState = usersAdapter.getInitialState({
loading: false,
error: null,
})

const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
userAdded: usersAdapter.addOne,
userUpdated: usersAdapter.updateOne,
userRemoved: usersAdapter.removeOne,
usersReceived: usersAdapter.setAll,
},
})

What It Gives You​

The adapter creates this state shape:

{
ids: [1, 2, 3], // Array of IDs in order
entities: { // Object with ID as key
1: { id: 1, name: 'John' },
2: { id: 2, name: 'Jane' },
3: { id: 3, name: 'Bob' },
},
loading: false, // Your extra fields
error: null,
}

Built-in Functions​

Adding Items​

// Add one item
usersAdapter.addOne(state, action) // action.payload = { id: 1, name: 'John' }

// Add many items
usersAdapter.addMany(state, action) // action.payload = [user1, user2, user3]

// Set all items (replaces everything)
usersAdapter.setAll(state, action) // action.payload = [user1, user2, user3]

Updating Items​

// Update one item
usersAdapter.updateOne(state, action) // action.payload = { id: 1, changes: { name: 'Johnny' } }

// Update many items
usersAdapter.updateMany(state, action) // action.payload = [{ id: 1, changes: {...} }, { id: 2, changes: {...} }]

Removing Items​

// Remove one item
usersAdapter.removeOne(state, action) // action.payload = 1 (the ID)

// Remove many items
usersAdapter.removeMany(state, action) // action.payload = [1, 2, 3] (array of IDs)

// Remove all items
usersAdapter.removeAll(state)

Complete Example​

import { createEntityAdapter, createSlice, createAsyncThunk } from '@reduxjs/toolkit'

// Create adapter
const postsAdapter = createEntityAdapter({
// Custom ID field (default is 'id')
selectId: (post) => post.postId,

// Sort posts by date
sortComparer: (a, b) => b.date.localeCompare(a.date),
})

// Async actions
export const fetchPosts = createAsyncThunk(
'posts/fetchPosts',
async () => {
const response = await fetch('/api/posts')
return response.json()
}
)

export const addPost = createAsyncThunk(
'posts/addPost',
async (postData) => {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(postData),
})
return response.json()
}
)

// Slice
const postsSlice = createSlice({
name: 'posts',
initialState: postsAdapter.getInitialState({
loading: false,
error: null,
}),
reducers: {
postUpdated: postsAdapter.updateOne,
postDeleted: postsAdapter.removeOne,
},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.loading = true
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.loading = false
postsAdapter.setAll(state, action.payload)
})
.addCase(fetchPosts.rejected, (state, action) => {
state.loading = false
state.error = action.error.message
})
.addCase(addPost.fulfilled, (state, action) => {
postsAdapter.addOne(state, action.payload)
})
},
})

export const { postUpdated, postDeleted } = postsSlice.actions
export default postsSlice.reducer

Getting Data Out​

The adapter gives you selectors to get data:

// Get selectors
export const {
selectAll: selectAllPosts,
selectById: selectPostById,
selectIds: selectPostIds,
selectEntities: selectPostEntities,
selectTotal: selectTotalPosts,
} = postsAdapter.getSelectors((state) => state.posts)

// Use in components
function PostList() {
const posts = useSelector(selectAllPosts)
const postCount = useSelector(selectTotalPosts)

return (
<div>
<p>Total posts: {postCount}</p>
{posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}

function PostDetail({ postId }) {
const post = useSelector(state => selectPostById(state, postId))

if (!post) return <div>Post not found</div>

return <div>{post.content}</div>
}

Custom Sorting​

const usersAdapter = createEntityAdapter({
// Sort by name alphabetically
sortComparer: (a, b) => a.name.localeCompare(b.name),
})

Custom ID Field​

const booksAdapter = createEntityAdapter({
// Use 'isbn' instead of 'id'
selectId: (book) => book.isbn,
})

Why Use Entity Adapter?​

  1. Normalized data - No duplicate items
  2. Fast lookups - Find items by ID quickly
  3. Automatic sorting - Keep items in order
  4. Less code - Built-in CRUD operations
  5. Performance - Optimized for large lists

When to Use​

  • Lists of items (users, posts, products)
  • Need to find items by ID
  • Want to avoid duplicate data
  • Need sorted lists
  • Working with APIs that return arrays