Creating Slices with createSlice
createSlice is the standard approach for writing Redux logic. It generates action creators and action types automatically based on reducer names.
Basic Slice
import { createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
},
},
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
Key Features
Immer Integration
- Mutative syntax: Write code that looks like it mutates state
- Immutable updates: Immer handles immutability automatically
- Simplified logic: No need for spread operators
Auto-generated Actions
// Actions are automatically created
counterSlice.actions.increment() // { type: 'counter/increment' }
counterSlice.actions.incrementByAmount(5) // { type: 'counter/incrementByAmount', payload: 5 }
Complex State Examples
Object State
const userSlice = createSlice({
name: 'user',
initialState: {
profile: null,
isLoading: false,
error: null,
},
reducers: {
setUser: (state, action) => {
state.profile = action.payload
},
updateProfile: (state, action) => {
Object.assign(state.profile, action.payload)
},
setLoading: (state, action) => {
state.isLoading = action.payload
},
setError: (state, action) => {
state.error = action.payload
},
},
})
Array State
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: (state, action) => {
state.push({
id: action.payload.id,
text: action.payload.text,
completed: false,
})
},
toggleTodo: (state, action) => {
const todo = state.find(todo => todo.id === action.payload)
if (todo) {
todo.completed = !todo.completed
}
},
removeTodo: (state, action) => {
return state.filter(todo => todo.id !== action.payload)
},
},
})
Prepare Callbacks
Customize action payloads with prepare callbacks:
const postsSlice = createSlice({
name: 'posts',
initialState: [],
reducers: {
addPost: {
reducer: (state, action) => {
state.push(action.payload)
},
prepare: (title, content) => {
return {
payload: {
id: nanoid(),
title,
content,
createdAt: new Date().toISOString(),
},
}
},
},
},
})
ExtraReducers
Handle actions from other slices or async thunks:
const postsSlice = createSlice({
name: 'posts',
initialState: {
posts: [],
status: 'idle',
},
reducers: {
// Regular reducers
},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.status = 'loading'
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded'
state.posts = action.payload
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
},
})
TypeScript with createSlice
interface CounterState {
value: number
}
const initialState: CounterState = {
value: 0,
}
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload
},
},
})
Best Practices
- One slice per feature
- Use descriptive names
- Keep reducers simple
- Use prepare callbacks for complex payloads
- Leverage TypeScript for type safety