Skip to main content

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

  1. One slice per feature
  2. Use descriptive names
  3. Keep reducers simple
  4. Use prepare callbacks for complex payloads
  5. Leverage TypeScript for type safety