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?
- Normalized data - No duplicate items
- Fast lookups - Find items by ID quickly
- Automatic sorting - Keep items in order
- Less code - Built-in CRUD operations
- 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