Understanding Redux Slices
A "slice" in Redux Toolkit represents a portion of your Redux state along with the reducers and actions that manipulate that state. It's a way to organize your Redux logic by feature or domain.
The createSlice function is the core API of Redux Toolkit that brings together:
- A slice name (used as the prefix for action types)
- An initial state value
- Reducer functions that define how state updates in response to actions
It automatically generates action creators and action types corresponding to the reducers you define.
Real-World Analogy: Department Management
Think of Redux slices like departments in a large company:
- Each department (slice) manages its own specific area of responsibility
- Each department has standard procedures (reducers) for handling different types of requests
- The company's organizational chart shows how all departments fit together (root state)
- Requests (actions) are directed to specific departments based on their type
- Each department has its own filing system and records (state)
Just as a well-organized company divides responsibilities among departments rather than handling everything centrally, a well-designed Redux store divides state management among slices rather than using a single monolithic reducer.
The createSlice API
The createSlice function accepts a single configuration object parameter with the following options:
import { createSlice } from '@reduxjs/toolkit';
const mySlice = createSlice({
// A name for this slice, used in action types
name: 'feature',
// The initial state for this slice
initialState: {
// Your state fields here
},
// An object containing Redux reducer functions
reducers: {
// Reducer functions here
},
// Extra reducers for handling actions defined outside the slice
extraReducers: (builder) => {
// Builder callback for handling other actions
}
});
// Destructure and export the generated action creators
export const { actionOne, actionTwo } = mySlice.actions;
// Export the reducer function for the whole slice
export default mySlice.reducer;
Key Parameters
- name (required): A string name for this slice, used as the prefix for generated action types
- initialState (required): The initial state value for this slice
- reducers (required): An object containing Redux reducer functions. Keys determine action type names.
- extraReducers (optional): Additional reducers that respond to actions defined elsewhere
Return Value
The createSlice function returns an object with the following properties:
- name: The slice name you provided
- reducer: The reducer function for the entire slice
- actions: An object containing the generated action creators
- caseReducers: The raw case reducer functions (rarely used directly)
// What createSlice returns
{
name: 'feature',
reducer: function reducer(state, action) { ... },
actions: {
actionOne: function actionOne(payload) { ... },
actionTwo: function actionTwo(payload) { ... }
},
caseReducers: {
actionOne: function actionOne(state, action) { ... },
actionTwo: function actionTwo(state, action) { ... }
}
}
Action Type Naming Convention
Action types are automatically generated using the slice name as a prefix:
slice.name+'/'+reducerName
For example, with a slice named 'todos' and a reducer named 'addTodo', the generated action type would be 'todos/addTodo'. This naming convention helps avoid action type collisions across different slices.
Creating a Basic Slice
Let's explore how to create a basic counter slice:
// features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
value: 0,
status: 'idle'
};
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
// Redux Toolkit allows us to write "mutating" logic in reducers.
// Under the hood, it uses Immer to make immutable updates.
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
reset: (state) => {
state.value = 0;
}
}
});
// Export the generated action creators
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;
// Export the reducer function
export default counterSlice.reducer;
Then you can add this slice reducer to your store:
// app/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer
}
});
Now you can use these action creators in your components:
// features/counter/Counter.jsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount } from './counterSlice';
export function Counter() {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<div>
<button onClick={() => dispatch(decrement())}>-</button>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
</div>
<button onClick={() => dispatch(incrementByAmount(5))}>
Add 5
</button>
</div>
);
}
Working with Action Payloads
Actions often need to include additional data (payload) beyond just the action type. With createSlice, you can easily work with action payloads:
// Slice with action payloads
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
// Action with a simple payload
addTodo: (state, action) => {
// action.payload is the argument passed when the action is dispatched
// e.g., dispatch(addTodo('Buy milk'))
state.push({
id: Date.now(),
text: action.payload,
completed: false
});
},
// Action with a complex payload object
updateTodo: (state, action) => {
// action.payload is an object with id and changes
// e.g., dispatch(updateTodo({ id: 123, changes: { text: 'New text' } }))
const { id, changes } = action.payload;
const todo = state.find(todo => todo.id === id);
if (todo) {
// Apply all changes to the todo
Object.assign(todo, changes);
}
},
// Action with multiple parameters
addDetailedTodo: {
// Prepare callback customizes the payload creation
prepare(text, description, priority) {
return {
payload: {
id: Date.now(),
text,
description,
priority,
completed: false,
createdAt: new Date().toISOString()
}
};
},
// Reducer function uses the prepared payload
reducer(state, action) {
state.push(action.payload);
}
}
}
});
export const { addTodo, updateTodo, addDetailedTodo } = todosSlice.actions;
// Usage in a component
dispatch(addTodo('Buy milk'));
dispatch(updateTodo({ id: 123, changes: { text: 'Buy almond milk', completed: true } }));
dispatch(addDetailedTodo('File taxes', 'Complete 2024 tax return', 'high'));
The Prepare Callback
When you need to customize how the action payload is created, you can use a "prepare callback" by providing an object with reducer and prepare functions instead of just a reducer function:
const postsSlice = createSlice({
name: 'posts',
initialState: [],
reducers: {
// Using a prepare callback to generate a UUID and timestamp
createPost: {
prepare(title, content, authorId) {
// Using nanoid from Redux Toolkit for unique IDs
const id = nanoid();
return {
payload: {
id,
title,
content,
authorId,
date: new Date().toISOString(),
reactions: { thumbsUp: 0, heart: 0, rocket: 0 }
}
};
},
reducer(state, action) {
state.push(action.payload);
}
},
// Simple reducer for comparison
deletePost(state, action) {
return state.filter(post => post.id !== action.payload);
}
}
});
export const { createPost, deletePost } = postsSlice.actions;
// Usage
dispatch(createPost('Redux Toolkit', 'Awesome library!', 'user123'));
dispatch(deletePost('post123'));
When to Use a Prepare Callback
Use a prepare callback when you need to:
- Accept multiple parameters for an action creator
- Generate random values like IDs or timestamps
- Perform transformations on inputs before they become the payload
- Add additional metadata to the action beyond the payload
For example, in an e-commerce app, an "addToCart" action might need to:
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] },
reducers: {
addToCart: {
prepare(productId, quantity = 1, options = {}) {
return {
payload: {
id: nanoid(),
productId,
quantity,
options,
addedAt: new Date().toISOString()
},
meta: {
analytics: {
event: 'add_to_cart',
productId,
quantity
}
}
};
},
reducer(state, action) {
state.items.push(action.payload);
}
}
}
});
This approach lets you create a clean API for your action creators while handling the complexity of payload creation internally.
Writing Immutable Updates with Immer
One of the most powerful features of Redux Toolkit is its integration with Immer, which allows you to write reducers that appear to "mutate" state but actually produce immutable updates.
How Immer Works with createSlice
In Redux, state updates must be immutable, which means creating new copies of objects and arrays instead of modifying the originals. This can lead to verbose code with lots of spreading and mapping. Immer simplifies this by letting you write code that looks like direct mutation, but behind the scenes it creates a new immutable state.
// Without Immer (traditional Redux)
const todosReducer = (state = [], action) => {
switch (action.type) {
case 'todos/addTodo':
return [
...state,
{
id: action.payload.id,
text: action.payload.text,
completed: false
}
];
case 'todos/toggleTodo':
return state.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
);
case 'todos/updateTodoText':
return state.map(todo =>
todo.id === action.payload.id
? { ...todo, text: action.payload.text }
: todo
);
default:
return state;
}
};
// With Immer (via Redux Toolkit)
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: (state, action) => {
// "Mutate" the state array by pushing a new item
state.push({
id: action.payload.id,
text: action.payload.text,
completed: false
});
},
toggleTodo: (state, action) => {
// Find the todo and toggle its completed flag
const todo = state.find(todo => todo.id === action.payload);
todo.completed = !todo.completed;
},
updateTodoText: (state, action) => {
// Find the todo and update its text
const todo = state.find(todo => todo.id === action.payload.id);
todo.text = action.payload.text;
}
}
});
Common Immutable Update Patterns with Immer
Here are some common patterns for updating state with Immer:
const appSlice = createSlice({
name: 'app',
initialState: {
users: [],
settings: {
theme: 'light',
notifications: {
email: true,
push: false,
sms: true
}
},
posts: {
byId: {},
allIds: []
}
},
reducers: {
// Adding to an array
addUser: (state, action) => {
state.users.push(action.payload);
},
// Removing from an array
removeUser: (state, action) => {
const index = state.users.findIndex(user => user.id === action.payload);
if (index !== -1) {
state.users.splice(index, 1);
}
},
// Updating a nested property
updateTheme: (state, action) => {
state.settings.theme = action.payload;
},
// Updating a deeply nested property
togglePushNotifications: (state) => {
state.settings.notifications.push = !state.settings.notifications.push;
},
// Working with an object map (normalized data)
addPost: (state, action) => {
const { id } = action.payload;
state.posts.byId[id] = action.payload;
state.posts.allIds.push(id);
},
// Updating an object in a normalized structure
updatePost: (state, action) => {
const { id, ...changes } = action.payload;
if (state.posts.byId[id]) {
// Merge changes into the existing post
Object.assign(state.posts.byId[id], changes);
}
}
}
});
Real-World Example: E-commerce Cart
Let's see how Immer simplifies managing a shopping cart in an e-commerce application:
const cartSlice = createSlice({
name: 'cart',
initialState: {
items: [],
couponCode: null,
discountPercentage: 0,
shipping: {
method: 'standard',
cost: 5.99
},
subtotal: 0,
tax: 0,
total: 0
},
reducers: {
addItem: (state, action) => {
const { productId, price, quantity = 1 } = action.payload;
// Check if item is already in cart
const existingItem = state.items.find(item => item.productId === productId);
if (existingItem) {
// Increase quantity if already in cart
existingItem.quantity += quantity;
} else {
// Add new item to cart
state.items.push({
id: Date.now(),
productId,
price,
quantity,
addedAt: new Date().toISOString()
});
}
// Recalculate cart totals
recalculateCart(state);
},
removeItem: (state, action) => {
// Remove item by ID
state.items = state.items.filter(item => item.id !== action.payload);
// Recalculate cart totals
recalculateCart(state);
},
updateQuantity: (state, action) => {
const { itemId, quantity } = action.payload;
// Find the item and update quantity
const item = state.items.find(item => item.id === itemId);
if (item) {
item.quantity = Math.max(1, quantity); // Ensure minimum quantity of 1
}
// Recalculate cart totals
recalculateCart(state);
},
applyCoupon: (state, action) => {
const { code, discountPercentage } = action.payload;
state.couponCode = code;
state.discountPercentage = discountPercentage;
// Recalculate cart totals
recalculateCart(state);
},
updateShipping: (state, action) => {
const { method, cost } = action.payload;
state.shipping.method = method;
state.shipping.cost = cost;
// Recalculate cart totals
recalculateCart(state);
},
clearCart: (state) => {
state.items = [];
state.couponCode = null;
state.discountPercentage = 0;
// Recalculate cart totals (will be zero)
recalculateCart(state);
}
}
});
// Helper function to recalculate cart totals
function recalculateCart(state) {
// Calculate subtotal
state.subtotal = state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
// Apply discount if available
const discount = state.subtotal * (state.discountPercentage / 100);
const discountedSubtotal = state.subtotal - discount;
// Calculate tax (example: 8% tax rate)
state.tax = discountedSubtotal * 0.08;
// Calculate total
state.total = discountedSubtotal + state.tax + state.shipping.cost;
}
With Immer, we can directly update nested properties (like item quantities) and even call helper functions that further modify the state, all while maintaining immutability behind the scenes.
Working with extraReducers
The extraReducers field lets your slice respond to actions that weren't defined as part of your slice's reducers field. This is useful for:
- Responding to actions created by
createAsyncThunk - Responding to actions defined in other slices
- Responding to actions from third-party libraries
Builder Callback Notation
The recommended way to use extraReducers is with the "builder callback" notation:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Create an async thunk for fetching users
export const fetchUsers = createAsyncThunk(
'users/fetchUsers',
async () => {
const response = await fetch('https://api.example.com/users');
return response.json();
}
);
const usersSlice = createSlice({
name: 'users',
initialState: {
entities: [],
loading: 'idle',
error: null
},
reducers: {
// Regular reducers here
userAdded: (state, action) => {
state.entities.push(action.payload);
}
},
extraReducers: (builder) => {
// Add reducers for additional action types
// When fetchUsers starts
builder.addCase(fetchUsers.pending, (state) => {
state.loading = 'loading';
state.error = null;
});
// When fetchUsers succeeds
builder.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = 'idle';
state.entities = action.payload;
});
// When fetchUsers fails
builder.addCase(fetchUsers.rejected, (state, action) => {
state.loading = 'idle';
state.error = action.error.message;
});
// You can also match against actions from other slices
builder.addCase('auth/logout', (state) => {
// Clear user data when the user logs out
state.entities = [];
});
// Match a range of action types using matchers
builder.addMatcher(
(action) => action.type.endsWith('/fulfilled'),
(state) => {
state.lastFetchSuccess = new Date().toISOString();
}
);
// Default case when no other handlers have run
builder.addDefaultCase((state, action) => {
// Optional: log unhandled actions
console.log('Unhandled action:', action.type);
});
}
});
The builder object provides several methods:
addCase(actionCreator, reducer): Handle a specific action typeaddMatcher(matcher, reducer): Handle actions that satisfy a matcher functionaddDefaultCase(reducer): Handle any action not already handled by cases or matchers
Real-World Example: Authentication Slice
In a real application, you might have an authentication slice that responds to both its own actions and actions from other parts of the app:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { api } from '../../api/apiClient';
// Async thunks for authentication
export const login = createAsyncThunk(
'auth/login',
async ({ email, password }, { rejectWithValue }) => {
try {
const response = await api.post('/auth/login', { email, password });
// Store token in localStorage
localStorage.setItem('token', response.data.token);
return response.data.user;
} catch (err) {
return rejectWithValue(err.response.data);
}
}
);
export const fetchUser = createAsyncThunk(
'auth/fetchUser',
async (_, { rejectWithValue }) => {
try {
const token = localStorage.getItem('token');
if (!token) return rejectWithValue('No token found');
const response = await api.get('/auth/me', {
headers: { Authorization: `Bearer ${token}` }
});
return response.data;
} catch (err) {
return rejectWithValue(err.response.data);
}
}
);
const authSlice = createSlice({
name: 'auth',
initialState: {
user: null,
token: localStorage.getItem('token'),
loading: 'idle',
error: null,
lastActivity: null
},
reducers: {
logout: (state) => {
localStorage.removeItem('token');
state.user = null;
state.token = null;
state.error = null;
},
clearError: (state) => {
state.error = null;
}
},
extraReducers: (builder) => {
// Login flow
builder
.addCase(login.pending, (state) => {
state.loading = 'loading';
state.error = null;
})
.addCase(login.fulfilled, (state, action) => {
state.loading = 'idle';
state.user = action.payload;
state.token = localStorage.getItem('token');
state.lastActivity = new Date().toISOString();
})
.addCase(login.rejected, (state, action) => {
state.loading = 'idle';
state.error = action.payload || action.error.message;
})
// Fetch user flow
.addCase(fetchUser.pending, (state) => {
state.loading = 'loading';
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = 'idle';
state.user = action.payload;
state.lastActivity = new Date().toISOString();
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = 'idle';
state.error = action.payload || action.error.message;
// If we got an authentication error, clear the user and token
if (action.payload === 'Unauthorized' ||
action.payload === 'Token expired' ||
action.payload === 'No token found') {
state.user = null;
state.token = null;
localStorage.removeItem('token');
}
})
// Respond to actions from other slices
.addCase('profile/updateSuccess', (state, action) => {
// Update user when profile is updated
if (state.user && state.user.id === action.payload.id) {
state.user = { ...state.user, ...action.payload };
}
})
// Track user activity on all successful actions
.addMatcher(
(action) => action.type.endsWith('/fulfilled') &&
action.type !== 'auth/logout',
(state) => {
state.lastActivity = new Date().toISOString();
}
);
}
});
export const { logout, clearError } = authSlice.actions;
export default authSlice.reducer;
This authentication slice handles login and user fetching, responds to profile updates from other slices, and tracks user activity across all successful actions in the app.
Using Selectors with Slices
Selectors are functions that extract specific pieces of data from the store state. When working with slices, it's a good practice to co-locate your selectors with your slice definition:
// features/posts/postsSlice.js
import { createSlice, createSelector } from '@reduxjs/toolkit';
const postsSlice = createSlice({
name: 'posts',
initialState: {
items: [],
loading: 'idle',
error: null,
filters: {
status: 'all',
category: null
}
},
reducers: {
// ... reducers here
setStatusFilter: (state, action) => {
state.filters.status = action.payload;
},
setCategoryFilter: (state, action) => {
state.filters.category = action.payload;
}
},
extraReducers: (builder) => {
// ... extraReducers here
}
});
// Export the action creators
export const { setStatusFilter, setCategoryFilter } = postsSlice.actions;
// Export the reducer
export default postsSlice.reducer;
// Simple selectors
export const selectAllPosts = state => state.posts.items;
export const selectPostById = (state, postId) =>
state.posts.items.find(post => post.id === postId);
export const selectPostsLoading = state => state.posts.loading;
export const selectPostsError = state => state.posts.error;
export const selectStatusFilter = state => state.posts.filters.status;
export const selectCategoryFilter = state => state.posts.filters.category;
// Memoized selector for filtered posts
export const selectFilteredPosts = createSelector(
[selectAllPosts, selectStatusFilter, selectCategoryFilter],
(posts, status, category) => {
return posts.filter(post => {
const statusMatch = status === 'all' || post.status === status;
const categoryMatch = !category || post.category === category;
return statusMatch && categoryMatch;
});
}
);
// Memoized selector for post statistics
export const selectPostStats = createSelector(
[selectAllPosts],
(posts) => {
return {
total: posts.length,
published: posts.filter(post => post.status === 'published').length,
draft: posts.filter(post => post.status === 'draft').length,
averageWordCount: posts.length > 0
? posts.reduce((sum, post) => sum + post.wordCount, 0) / posts.length
: 0
};
}
);
Using these selectors in components:
// features/posts/PostsList.jsx
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
selectFilteredPosts,
selectPostsLoading,
selectPostsError,
selectStatusFilter,
selectCategoryFilter,
setStatusFilter,
setCategoryFilter,
fetchPosts
} from './postsSlice';
import PostItem from './PostItem';
import Filters from './Filters';
export function PostsList() {
const dispatch = useDispatch();
// Use the selectors to extract data from the store
const posts = useSelector(selectFilteredPosts);
const loading = useSelector(selectPostsLoading);
const error = useSelector(selectPostsError);
const statusFilter = useSelector(selectStatusFilter);
const categoryFilter = useSelector(selectCategoryFilter);
useEffect(() => {
if (loading === 'idle') {
dispatch(fetchPosts());
}
}, [loading, dispatch]);
// Render the component based on the selected data
// ...
}
Benefits of Co-Located Selectors
Keeping selectors with your slice definitions offers several advantages:
- Encapsulation: Components don't need to know about the state structure
- Reusability: Selectors can be used across multiple components
- Maintainability: When the state shape changes, you only need to update the selectors in one place
- Performance: Memoized selectors prevent unnecessary recalculations
- Testing: Selectors can be tested independently of components
For example, if you decide to change your posts from an array to a normalized structure with byId and allIds, you would only need to update the selectors, and all components using those selectors would continue to work without changes.
Organizing Slices in a Real Application
As your application grows, organizing your Redux code becomes increasingly important. Here's a recommended structure for a medium to large application:
src/
├── app/
│ ├── store.js # Store setup with configureStore
│ └── rootReducer.js # Root reducer composition (optional)
│
├── features/ # Feature folders
│ ├── auth/
│ │ ├── authSlice.js # Slice for authentication
│ │ ├── Login.jsx # Related components
│ │ ├── Register.jsx
│ │ └── authAPI.js # API calls related to auth
│ │
│ ├── posts/
│ │ ├── postsSlice.js # Slice for posts
│ │ ├── PostsList.jsx # Related components
│ │ ├── PostDetail.jsx
│ │ └── postsAPI.js # API calls related to posts
│ │
│ └── users/
│ ├── usersSlice.js # Slice for users
│ ├── UserProfile.jsx # Related components
│ └── usersAPI.js # API calls related to users
│
└── common/ # Shared code
├── components/ # Reusable UI components
├── hooks/ # Custom hooks
└── utils/ # Utility functions
Combining Multiple Slices
In your store setup, you'll combine all your slice reducers:
// app/store.js
import { configureStore } from '@reduxjs/toolkit';
import authReducer from '../features/auth/authSlice';
import postsReducer from '../features/posts/postsSlice';
import usersReducer from '../features/users/usersSlice';
import uiReducer from '../features/ui/uiSlice';
export const store = configureStore({
reducer: {
auth: authReducer,
posts: postsReducer,
users: usersReducer,
ui: uiReducer
}
});
// Optional: for TypeScript support
export type RootState = ReturnType;
export type AppDispatch = typeof store.dispatch;
Feature-First vs. Function-First Organization
There are two main approaches to organizing Redux code:
- Feature-First (shown above): Group all files related to a feature (slice, components, API) together. This makes it easier to understand and work on a specific feature.
- Function-First: Group files by their function (all slices together, all components together). This can be better for smaller apps or when features have lots of overlap.
Most Redux Toolkit applications use the feature-first approach because it aligns well with the concept of slices and makes the codebase more maintainable as it grows.
Slice Design Best Practices
When designing your slices, consider these best practices:
- Keep slices focused: Each slice should manage one domain or feature
- Use meaningful slice names: Names should clearly indicate what the slice is responsible for
- Co-locate related logic: Keep actions, reducers, selectors, and thunks for a feature together
- Model your state shape carefully: Consider normalization for relational data
- Minimize cross-slice dependencies: Use extraReducers to respond to actions from other slices
- Export what's needed: Only export the slice reducer and action creators that need to be used elsewhere
- Consider slice size: Split very large slices into sub-slices if they become unwieldy
For example, if your "users" slice is getting too large, you might split it into "users", "userPreferences", and "userActivity" slices, each focused on a specific aspect of user management.
Practice Activities
Activity 1: Create a Todo Slice
Build a complete todo list slice with the following features:
- Initial state with todos array and filter status
- Reducers for adding, toggling, editing, and removing todos
- A prepare callback for generating todo IDs and timestamps
- Selectors for getting all todos, filtered todos, and todo statistics
- Connect the slice to a store and create a simple UI to test it
Activity 2: Working with Nested Data
Create a "blog" slice that handles more complex data:
- Store posts, comments, and authors in a normalized structure
- Create reducers for adding/editing/removing posts and comments
- Implement nested updates (e.g., adding a comment to a specific post)
- Use extraReducers to handle related actions (e.g., updating author info in posts)
- Create denormalizing selectors to recombine data for display
Activity 3: Simulate a Full Application
Build a simulated e-commerce application with multiple slices:
- Products slice (fetching and filtering products)
- Cart slice (adding, removing, and updating quantities)
- User slice (authentication and user preferences)
- UI slice (modal state, notifications, loading indicators)
- Make them interact appropriately (e.g., clearing cart on logout)
- Organize the slices using the feature-first approach
Summary
- Redux slices are portions of Redux state with their associated reducers and actions
- createSlice combines action creators, action types, and reducers into a single function call
- Immer integration in createSlice allows you to write "mutating" code that produces immutable updates
- Prepare callbacks let you customize action payloads and accept multiple parameters
- extraReducers lets your slice respond to actions defined elsewhere
- Co-locating selectors with slices improves maintainability and encapsulation
- Feature-first organization groups related Redux code by feature rather than function
- Well-designed slices lead to more maintainable Redux applications