b
Many reducers
Let's continue building a simplified redux version of our tasks application.
As a reminder, the Redux store brings together:
To ease development, let's initialize our redux store's state in reducers/taskReducer.js.
Let's change taskReducer's default state from an empty array [] to an array of task objects initialState:
const initialState = [
{
content: 'remind myself that the reducer defines how the redux store works',
important: true,
id: 1,
},
{
content: 'repeat the words: a redux store can contain any data',
important: false,
id: 2,
},
]
const taskReducer = (state = initialState, action) => { // ...
}
// ...
export default taskReducerStore with complex state
Let's implement filtering for the tasks that are displayed to the user. The user interface for the filters will be implemented with radio buttons:

Let's start with a very simple and straightforward implementation in App.js:
import NewTask from './components/NewTask'
import Tasks from './components/Tasks'
const App = () => {
const filterSelected = (value) => { console.log(value) }
return (
<div>
<NewTask />
<div> <input type="radio" name="filter" onChange={() => filterSelected('ALL')} />all <input type="radio" name="filter" onChange={() => filterSelected('IMPORTANT')} />important <input type="radio" name="filter" onChange={() => filterSelected('UNIMPORTANT')} />unimportant </div> <Tasks />
</div>
)
}Since the name attribute of all the radio buttons is the same, the three options form a button group where only one option can be selected.
The buttons have a change handler that currently only prints the string associated with the clicked button to the console.
We decide to implement the filter functionality by storing the value of the filter in the redux store in addition to the tasks themselves. The state of the store should look like this after we finish making the changes in the next section:
{
tasks: [
{ content: 'remind myself that the reducer defines how the redux store works', important: true, id: 1},
{ content: 'repeat the words: a redux store can contain any data', important: false, id: 2}
],
filter: 'IMPORTANT'
}Currently, our application only stores the array of tasks. In the new implementation, the state object will have two properties:
tasksthat contains the array of tasksfilterthat contains a string indicating which tasks should be displayed to the user.
Combined reducers
To handle our new filter data, we could modify taskReducer to deal with the filter data as well.
However, a better solution in this situation is to separate the filter into a new file src/reducers/filterReducer.js:
const filterReducer = (state = 'ALL', action) => {
switch (action.type) {
case 'SET_FILTER':
return action.payload
default:
return state
}
}The actions for changing the filter's state look like this:
{
type: 'SET_FILTER',
payload: 'IMPORTANT'
}Let's also *create a new action creator function*.
We will write the code for the action creator after our filterReducer:
const filterReducer = (state = 'ALL', action) => {
// ...
}
export const filterChange = filter => {
return {
type: 'SET_FILTER',
payload: filter,
}
}
export default filterReducerWe can create the actual reducer for our application by combining the two existing reducers with the combineReducers function.
Let's define the combined reducer in main.jsx:
import React from 'react'
import ReactDOM from 'react-dom/client'
import { createStore, combineReducers } from 'redux'import { Provider } from 'react-redux'
import App from './App'
import taskReducer from './reducers/taskReducer'
import filterReducer from './reducers/filterReducer'
const reducer = combineReducers({ tasks: taskReducer, filter: filterReducer})
const store = createStore(reducer)
console.log(store.getState())
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
{/*<App />*/}
<div />
</Provider>
)Since our application breaks at this point, we render a
<div />element, commenting out our<App />. Remember that you can toggle multiline comments with Ctrl-Shift-/.You could also use Ctrl-/, which will comment out lines individually.
The state of the store gets printed to the console:

As we can see from the output, the store has the exact form we wanted it to!
Let's review how we were able to get this object by examining the
reducerwe passed tocreateStore,combineReducersconst reducer = combineReducers({ tasks: taskReducer, filter: filterReducer, })
combineReducershelped us create a state object with two properties:tasksandfilter. The value of thetasksproperty is defined by thetaskReducer, which does not have to deal with the other properties of the state. Likewise, thefilterproperty is managed by thefilterReducer.
Combined reducers in action
In this section we're going to take a step back from our project to investigate how the combined reducer works. Let's simulate changing the filter and creating a task by adding the following to the main.jsx file:
import { createTask } from './reducers/taskReducer'
import { filterChange } from './reducers/filterReducer'
//...
store.subscribe(() => console.log(store.getState()))
store.dispatch(filterChange('IMPORTANT'))
store.dispatch(createTask('remember that combineReducers forms one reducer from many simple reducers'))Notice that with our subscribe call above, the store's state gets logged to the console after every change:

At this point, it is good to become aware of a tiny but important detail.
If we add a console.log statement to the beginning of both reducers:
const filterReducer = (state = 'ALL', action) => {
console.log('ACTION: ', action)
// ...
}Based on the console output one might think that every action gets duplicated:

Is there a bug in our code? No.
The combined reducer works in such a way that every action gets handled in every part of the combined reducer.
Typically only one reducer is interested in any given action,
but there are situations where multiple reducers change their respective parts of the state based on the same action.
Finishing the filters
Let's finish the application so that it uses the combined reducer. We start by changing the rendering of the application and hooking up the store to the application in the main.jsx file:
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
)Next, let's fix a bug that is caused by the code expecting the application store to be an array of tasks:

Because the tasks are now embedded further into the state object, let's tweak the selector function:
const Tasks = () => {
const dispatch = useDispatch()
const tasks = useSelector(state => state.tasks)
return(
<ul>
{tasks.map(task =>
<Task
key={task.id}
task={task}
handleClick={() =>
dispatch(toggleImportanceOf(task.id))
}
/>
)}
</ul>
)
}Previously the selector function returned the whole state of the store:
const tasks = useSelector(state => state)And now it returns only its field tasks
const tasks = useSelector(state => state.tasks)Visibility Filter
Let's extract the visibility/importance filter into its own src/components/VisibilityFilter.js component:
import { filterChange } from '../reducers/filterReducer'
import { useDispatch } from 'react-redux'
const VisibilityFilter = (props) => {
const dispatch = useDispatch()
return (
<div>
<input
type="radio"
name="filter"
onChange={() => dispatch(filterChange('ALL'))}
/>
all
<input
type="radio"
name="filter"
onChange={() => dispatch(filterChange('IMPORTANT'))}
/>
important
<input
type="radio"
name="filter"
onChange={() => dispatch(filterChange('UNIMPORTANT'))}
/>
unimportant
</div>
)
}
export default VisibilityFilterWith the new component App can be simplified as follows:
import Tasks from './components/Tasks'
import NewTask from './components/NewTask'
import VisibilityFilter from './components/VisibilityFilter'
const App = () => {
return (
<div>
<NewTask />
<VisibilityFilter />
<Tasks />
</div>
)
}
export default AppNow, clicking the different radio buttons changes the state of the store's filter property via the dispatch call.
Let's change the Tasks component's useSelector from this:
useSelector(state => state.tasks)to incorporating our visibility filter, which we'll embed in the useSelector directly.
const Tasks = () => {
const dispatch = useDispatch()
const tasks = useSelector(state => { if ( state.filter === 'ALL' ) { return state.tasks } return state.filter === 'IMPORTANT' ? state.tasks.filter(task => task.important) : state.tasks.filter(task => !task.important) })
return(
<ul>
{tasks.map(task =>
<Task
key={task.id}
task={task}
handleClick={() =>
dispatch(toggleImportanceOf(task.id))
}
/>
)}
</ul>
)We can simplify useSelector even further by destructuring state's parameters:
const tasks = useSelector(({ filter, tasks }) => {
if ( filter === 'ALL' ) {
return tasks
}
return filter === 'IMPORTANT'
? tasks.filter(task => task.important)
: tasks.filter(task => !task.important)
})There is a slight cosmetic flaw in our application.
Even though the filter is set to ALL by default, the associated radio button is not selected.
Naturally, this issue can be fixed, but since this is relatively harmless we will save the fix for later.
The current version of the application can be found on GitHub, branch part6-2.
Redux Toolkit
As we have seen so far, Redux's configuration and state management implementation requires some effort. For example, the reducer and action creator-related code has somewhat repetitive boilerplate code. Redux Toolkit is a library that solves these common Redux-related problems. This library simplifies the Redux store's configuration and offers a large variety of tools to ease state management.
Let's start using Redux Toolkit in our application by refactoring the existing code. First, we will need to install the library:
npm i @reduxjs/toolkitNext, open the main.jsx file which currently creates the Redux store.
Instead of Redux's createStore function, let's create the store using Redux Toolkit's configureStore function in main.jsx:
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { configureStore } from '@reduxjs/toolkit'import App from './App'
import taskReducer from './reducers/taskReducer'
import filterReducer from './reducers/filterReducer'
const store = configureStore({ reducer: { tasks: taskReducer, filter: filterReducer }})
console.log(store.getState())
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
)We already got rid of a few lines of code now that we don't need the combineReducers function to create the reducer for the store.
We will soon see that the configureStore function has many additional benefits such as the effortless integration of development tools
and many commonly used libraries without the need for additional configuration.
Refactoring our Reducers with Redux Toolkit
With Redux Toolkit, we can easily create reducer and related action creators using the createSlice function.
Let's use createSlice to refactor the reducer and action creators in the reducers/taskReducer.js file:
import { createSlice } from '@reduxjs/toolkit'
const initialState = [
{
content: 'remind myself that the reducer defines how the redux store works',
important: true,
id: 1,
},
{
content: 'repeat the words: a redux store can contain any data',
important: false,
id: 2,
},
]
const generateId = () =>
Number((Math.random() * 1000000).toFixed(0))
const taskSlice = createSlice({ name: 'tasks', initialState, reducers: { createTask(state, action) { const content = action.payload state.push({ content, important: false, id: generateId(), }) }, toggleImportanceOf(state, action) { const id = action.payload const taskToChange = state.find(t => t.id === id) const changedTask = { ...taskToChange, important: !taskToChange.important } return state.map(task => task.id !== id ? task : changedTask ) } },})The createSlice function's name parameter defines the prefix which is used in the action's type values.
From the code above, notice our object has name: 'tasks'; thus the createTask action will have the type value of tasks/createTask.
It is a good practice to give a unique name property.
That way there won't be unexpected collisions between the application's action type values.
The initialState parameter defines the reducer's initial state.
The reducers parameter takes the reducer itself as an object, where we define how functions handle state changes caused by certain actions.
Notice that the action.payload in the function contains the argument provided by calling the action creator:
dispatch(createTask('Preach about how awesome Redux Toolkit is!'))This dispatch call responds to dispatching the following object:
dispatch({ type: 'tasks/createTask', payload: 'Preach about how awesome Redux Toolkit is!' })Redux Toolkit and Immutability
If you followed closely, you might have noticed that inside the createTask action,
we see code that seems to violate the reducers' immutability principle mentioned earlier:
createTask(state, action) {
const content = action.payload
state.push({
content,
important: false,
id: generateId(),
})
}We are mutating state argument's array by calling the push method instead of returning a new instance of the array. 😱
What's this all about?
Redux Toolkit utilizes the Immer library with reducers created by the createSlice function.
Using this library makes it possible to mutate the state argument inside of createSlice.
Immer uses the mutated state to produce a new, immutable state and thus the state changes remain immutable.
Notice that state can be changed without mutating it, as we have done with the toggleImportanceOf action.
In this case, the function returns the new state.
Nevertheless, mutating the state will often come in handy especially when a complex state needs to be updated.
The createSlice function returns an object containing the reducer as well as the action creators defined by the reducers parameter.
We can access the reducer via taskSlice.reducer and the action creators via taskSlice.actions.
We can produce the file's exports in the following way:
const taskSlice = createSlice(/* ... */)
export const { createTask, toggleImportanceOf } = taskSlice.actionsexport default taskSlice.reducerNow, the imports in the other files will work just like before:
import taskReducer, { createTask, toggleImportanceOf } from './reducers/taskReducer'Nonetheless, we need to alter the action type names in the tests due to the conventions of ReduxToolkit:
import taskReducer from './taskReducer'
import deepFreeze from 'deep-freeze'
describe('taskReducer', () => {
test('returns new state with action tasks/createTask', () => {
const state = []
const action = {
type: 'tasks/createTask', payload: 'learn more about how the app state is in redux store', }
deepFreeze(state)
const newState = taskReducer(state, action)
expect(newState).toHaveLength(1)
expect(newState.map(s => s.content)).toContainEqual(action.payload)
})
test('returns new state with action tasks/toggleImportanceOf', () => {
const state = [
{
content: 'learn more about how the app state is in redux store',
important: true,
id: 1
},
{
content: 'understand more fully how state changes are made with actions',
important: false,
id: 2
}]
const action = {
type: 'tasks/toggleImportanceOf', payload: 2
}
deepFreeze(state)
const newState = taskReducer(state, action)
expect(newState).toHaveLength(2)
expect(newState).toContainEqual(state[0])
expect(newState).toContainEqual({
content: 'understand more fully how state changes are made with actions',
important: true,
id: 2
})
})
})Redux Toolkit and console.log
As we have learned, console.log has been a handy tool.
Let's try to print the state of the Redux Store to the console in the middle of the reducer created with the function createSlice:
const taskSlice = createSlice({
name: 'tasks',
initialState,
reducers: {
// ...
toggleImportanceOf(state, action) {
const id = action.payload
const taskToChange = state.find(n => n.id === id)
const changedTask = {
...taskToChange,
important: !taskToChange.important
}
console.log(state)
return state.map(task =>
task.id !== id ? task : changedTask
)
}
},
})The following is printed to the console

The output is interesting but not very useful. The reason we don't see any nice information is because of the Immer library used by the Redux Toolkit, which is now used internally to save the state of the Store.
The status can be converted to a human-readable format by converting state to a string and then back to a JavaScript object as follows:
console.log(JSON.parse(JSON.stringify(state)))Console output is now human-readable

Redux DevTools
Redux DevTools is a Chrome addon that offers useful development tools for Redux.
It can be used to inspect the Redux store's state and dispatch actions through the browser's console.
When the store is created using Redux Toolkit's configureStore function, no additional configuration is needed for Redux DevTools to work.
Once the addon is installed, clicking the Redux tab in the browser's console should open the development tools:

You can inspect how dispatching a certain action changes the state by clicking the action:

It is also possible to dispatch actions to the store using the development tools:

You can find the code for our current application in its entirety in the part6-3 branch of this GitHub repository.

