Reducing Redux Boilerplate with Redux Toolkit's createSlice
Learn how Redux Toolkit's createSlice cuts Redux boilerplate, uses Immer for immutable updates, and simplifies React state management with a practical todo-list example.
30 Jul 2026, 09:36 UTC

Problem: Verbose Redux code slows feature development
When a React application grows, managing state with plain Redux often means writing action type constants, action creators, and a reducer with a large switch statement. This boilerplate makes the code harder to read, increases the chance of typos, and obscures the actual state-transition logic.
Thesis: createSlice encapsulates actions and reducers
Redux Toolkit's createSlice API generates action creators and action types automatically, uses the Immer library so you can write mutable-looking reducer logic that produces immutable updates, and keeps Redux's predictable one-way data flow intact.
Setting up Redux Toolkit in a React project
First, add the toolkit and the React bindings. Run this in your project root (you need write access to the project's node_modules folder):
npm install @reduxjs/toolkit react-redux
Then configure the store. The configureStore helper accepts a reducer object and sets up good defaults, including the redux-thunk middleware and Redux DevTools integration in development.
// src/app/store.js
import { configureStore } from '@reduxjs/toolkit'
import todoReducer from '../features/todos/todoSlice'
export const store = configureStore({
reducer: {
todos: todoReducer,
},
})
Make the store available to the component tree with Provider from react-redux:
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { store } from './app/store'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App />
</Provider>
)
Writing a feature slice with createSlice
A slice groups the reducer logic and action creators for one domain. With createSlice you define the initial state and reducers; Immer lets you write mutating code that is safely converted to immutable updates.
// src/features/todos/todoSlice.js
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
items: [],
}
export const todoSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addTodo: (state, action) => {
// Immer allows this mutable-style update
state.items.push({ id: Date.now(), text: action.payload, completed: false })
},
toggleTodo: (state, action) => {
const todo = state.items.find(t => t.id === action.payload)
if (todo) {
todo.completed = !todo.completed
}
},
},
})
// Action creators are generated automatically
export const { addTodo, toggleTodo } = todoSlice.actions
export default todoSlice.reducer
Worked example: Todo list component using the slice
The component reads the todo list with useSelector and dispatches the slice's actions via useDispatch. Because the reducer logic lives in the slice, the component stays focused on UI concerns.
// src/components/TodoList.js
import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { addTodo, toggleTodo } from '../features/todos/todoSlice'
export default function TodoList() {
const dispatch = useDispatch()
const [input, setInput] = useState('')
const todos = useSelector(state => state.todos.items)
const handleSubmit = e => {
e.preventDefault()
if (input.trim()) {
dispatch(addTodo(input))
setInput('')
}
}
return (
<div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="What needs to be done?"
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map(todo => (
<li
key={todo.id}
onClick={() => dispatch(toggleTodo(todo.id))}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</li>
))}
</ul>
</div>
)
}
Trade-off / Limitation
While createSlice reduces boilerplate, it abstracts the raw Redux pattern. New developers still need to understand that the generated action creators and reducer are plain Redux concepts; debugging async logic still requires understanding middleware. The toolkit also adds to your bundle (commonly cited around 10 KB gzipped for the core library, though the exact figure depends on your bundler and version — measure it rather than trusting any single number).
Actionable closing
- Pick one existing Redux slice in your codebase.
- Replace its manual action types and reducer with a
createSlicedefinition as shown above. - Run the application (
npm start) and verify behavior matches the original implementation. - Open Redux DevTools to confirm that dispatched actions have the expected payloads and that state transitions are correct.
- If you use a bundler like webpack, run
webpack-bundle-analyzerbefore and after adding the toolkit to confirm the size increase stays within your budget. - Gradually migrate other slices, keeping an eye on bundle size and team familiarity.
By iteratively adopting createSlice you keep the benefits of Redux's predictable state updates while writing less repetitive code.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.