React Redux

This tutorial explores React-Redux in practice by building a step by step example of a todo list app. If you need are new to Redux start here.

View sandbox


The Setup

Let's first setup our environment by using Create React App:

 npx create-react-app todoapp
 cd todoapp

Redux has no relationship with React. The React-Redux library is designed to work with React's component model. It uses wrapper components that manage the Redux unidirectional data flow for us.

React Redux 6.x requires React 16.4 or later.

Install Redux and React-Redux:

npm install redux react-redux

Open the todoapp in your IDE. <br> Inside the ./src directory delete all the files to have a clean slate.

Create ./components/App.js and our app entry point ./src/index.js.

todoapp |- public |- /src + |- index.js + |- components + |- App.js |- .gitignore |- package.json |- package-lock.json |- README.md |- yarn.lock

App.js

import React from "react" const App = () => { return <div>"Entry Point"</div> } export default App

index.js

import React from "react" import ReactDOM from "react-dom" import App from "./components/App" ReactDOM.render(<App />, document.querySelector("#root"))

Material UI

We're going to use Material Design to style our components.<br> Install Material UI:

npm install @material-ui/core

Load the Roboto font and Material Icons on ./public/index.html.

<head> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" /> <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" /> </head>

Install SVG icons:

npm install @material-ui/icons

Start the server npm run start and we're ready to rock & roll!! :metal:

First, we're going to write our Redux logic. Then we'll bring React into the picture. Stop the server Ctrl+C for now.


Actions

In Redux, we can only change the state by emitting actions. Actions are plain JS objects that describe the change. Action Creators are functions that simply return the action.

Create our todoActions and a constants files to keep all the actions in one place and avoid typos.

todoapp |- /src + |- /redux + |- actions + |- todoActions.js //action creators + |- constants + |- constants.js //action type constants

On our todoapp we'll add functionality for adding, deleting, updating, and filtering tasks. Let's add these actions to our constants file:

constants.js

export const ADD_TODO = "ADD_TODO" export const UPDATE_TODO = "UPDATE_TODO" export const DELETE_TODO = "DELETE_TODO" export const TOGGLE_DONE_TODO = "TOGGLE_DONE_TODO" export const SET_FILTER_TODOS = "SET_FILTER_TODOS" export const SHOW_ALL_TODOS = "SHOW_ALL" export const SHOW_ACTIVE_TODOS = "SHOW_ACTIVE" export const SHOW_COMPLETED_TODOS = "SHOW_COMPLETED"

The only requirement in the action object is that it must have a type property, this will be use by the reducers to identify the dispatched action. The rest of the action object structure is up to the needs of our operation.

Let's make our action creators functions and export them.

todoActions.js

//import action types constants import { ADD_TODO, UPDATE_TODO, DELETE_TODO, TOGGLE_DONE_TODO, SET_FILTER_TODOS, } from "../constants/constants" //addTodo action creator let nextTodoId = 0 export const addTodo = content => { return { type: ADD_TODO, payload: { content, id: nextTodoId++, }, } } //updateTodo action creator export const updateTodo = (id, content) => { return { type: UPDATE_TODO, payload: { id, content, }, } } //deleteTodo action creator export const deleteTodo = id => { return { type: DELETE_TODO, payload: { id, }, } } //completedTodo action creator export const toggleDoneTodo = id => { return { type: TOGGLE_DONE_TODO, payload: { id, }, } } //filterTodos export const filterTodos = filter => { return { type: SET_FILTER_TODOS, filter, } }

Reducer

Reducers are pure functions that manage how the app's state changes in response to actions. We use multiple reducers to handle different parts of the app's state logic. If we have more than one Reducer, we need to combine them all into one using the combineReducers function.

The reducers functions need a default value to avoid any undefined errors when they run for the first time. We'll create a separate file to keep our state default values in one place.

Let's create the reducers and initial state files for our todoapp.

todoapp |- /src |- /redux ... + |- /reducers + |- index.js //root reducer(combined reducers) + |- todoFilterReducer.js + |- todoReducer.js + |- /store + |- initialState.js //initial state

initialState.js

import { SHOW_ALL_TODOS } from "../constants/constants" export default { todos: [], todosFilter: SHOW_ALL_TODOS, }

Reducers always receive two arguments in the same order: the previous state and an action. After the reducers do their operation they must return the next state or the previous state if nothing has changed. Never mutate the state.

todoReducer.js

// constant types import { ADD_TODO, UPDATE_TODO, DELETE_TODO, TOGGLE_DONE_TODO, } from "../constants/constants" // initial state import initalState from "../store/initialState" // todoReducer export default function (state = initalState.todos, action) { // deconstructing action object const { payload } = action // checking action type switch (action.type) { // add_todo reducer case ADD_TODO: return [ ...state, { id: payload.id, completed: false, content: payload.content, }, ] // update_todo reducer case UPDATE_TODO: return state.map(todo => { return todo.id === action.payload.id ? { ...todo, content: payload.content } : todo }) // delete_todo reducer case DELETE_TODO: return state.filter(todo => todo.id !== payload.id) // toggle_todo reducer case TOGGLE_DONE_TODO: return state.map(todo => { return todo.id === payload.id ? { ...todo, completed: !todo.completed } : todo }) // default state default: return state } }

todoFilterReducer.js

import { SET_FILTER_TODOS } from "../constants/constants" import initalState from "../store/initialState" const todoFilterReducer = (state = initalState.todosFilter, action) => { switch (action.type) { case SET_FILTER_TODOS: return action.filter default: return state } } export default todoFilterReducer

Root Reducer

We can make a single reducer with the combineReducers function. This function expects an object with all our imported reducers.

index.js (root reducer)

import { combineReducers } from "redux" import todosReducer from "./todoReducer" import todosFilterReducer from "./todoFilterReducer" /* This key names will used in our React components to access the state */ const rootReducer = combineReducers({ todos: todosReducer, todosFilter: todosFilterReducer, }) export default rootReducer

Store

The store holds the entire state tree of our app. We can only have one store in our entire app. Let's create a file for our store.

todoapp |- /src |- /redux ... |- /store |- initialState.js + |- index.js //store

We create a store with the createStore function and pass our rootReducer.

.src/redux/store/index.js (store)

import { createStore } from "redux" import rootReducer from "../reducers" export default () => { return createStore(rootReducer) }

We can pass two additional arguments to the createStore function.

1 - PreloadState (initial state)<br> For our todoapp We will pass the object we created on initialState.js. On universal apps we may optionally specify to hydrate the state from the server.

2 - Enhancer <br> The Enhancer is used to add third-party capabilities to the store, such as: middleware, time travel, persistence, etc. The only enhancer that ships with Redux is applyMiddleWare. Let's add Redux DevTools to help us debug our state changes. View Docs for API reference.

npm install --save-dev redux-devtools-extension

.src/redux/store/index.js (store)

import { createStore } from "redux" import rootReducer from "../reducers" import initialState from "./initialState" import { composeWithDevTools } from "redux-devtools-extension" export default () => { return createStore(rootReducer, initialState, composeWithDevTools()) }

PROVIDER

The <Provider /> component makes the Redux store accessible in our React components. The Provider accepts our store via props.

We created our store in it's own file. This pattern will keep our code more organize once our configuration gets more complex.

index.js (entry point)

import React from "react" import ReactDOM from "react-dom" import { Provider } from "react-redux" import App from "./components/App" import configureStore from "./redux/store" const store = configureStore() ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") )

This completes the Redux boiler plate of our application.


REACT

React Redux is designed to work with React's component model.

Our container components will subscribe to our Redux Store and dispatch the actions. Our presentational components will read data and invoke callbacks from props.

Let's add the files for our todoapp React components:

todoapp |- /src |- components |- App.js + |- /Todos + |- addTodos.js + |- filterTodos.js + |- todoEmptyState.js + |- todoItem.js + |- todos.js ...

Connect

To subscribe our React container components to Redux, we use the connect() function provided by React Redux.

import { connect } from "react-redux"

The connect function can take two function arguments: mapStateToProps and mapDispatchToProps.

//no second argument connect(mapStateToProps)(MyComponent) //or no first argument connect(null, mapDispatchToProps)(MyComponent)

mapStateToProps

The mapStateToProps function is used for taking relevant data from the store and making it available in our components and we can pass two arguments.

1. state <br> The first argument will return the entire Redux store state with the keys that we defined on combineReducers(root reducer).

2. ownProps (optional) <br> The second argument is called ownProps(optional). This is used if our component needs data from its own props to retrieve data from the store.

The mapStateToProps function should return a plain object containing the data needed for our component.

example:

function mapStateToProps(state) { const { todos } = state return { todos, } }

mapDispatchToProps

mapDispatchToProps is the second function argument we can pass to connect. It's used for dispatching actions to the store.

The are a couple of approaches for dispatching. We're going to use BindingActionCreators function to invoke our action creators directly. View other options.

example:

import { connect } from "react-redux" import { bindActionCreators } from "redux" //action creators import { updateTodo, deleteTodo, toggleDoneTodo, } from "../../redux/actions/todoActions" //return action creators function mapDispatchToProps(dispatch) { return bindActionCreators( { deleteTodo, updateTodo, toggleDoneTodo, }, dispatch ) }

Exporting our component

export default connect(mapStateToProps, mapDispatchToProps)(Todos)

Next, let's implement connect to our React components.


Todos container

This container component will be dispatching actions from the callbacks of the todoItem component. On the mapStateToProps we are destructuring the state and using setFilterTodos to filter the list of todos that we iterate and render.

todos.js

import React, { Component } from "react" import { connect } from "react-redux" import { bindActionCreators } from "redux" //constants import { SHOW_ALL_TODOS, SHOW_COMPLETED_TODOS, SHOW_ACTIVE_TODOS, } from "../../redux/constants/constants" //action creators import { updateTodo, deleteTodo, toggleDoneTodo, } from "../../redux/actions/todoActions" //material-ui components import List from "@material-ui/core/List" import Divider from "@material-ui/core/Divider" //our components import TodoItem from "./todoItem" import TodoEmptyState from "./todoEmptyState" class Todos extends Component { //render TodoItems renderTodos() { const { todos, deleteTodo, updateTodo, toggleDoneTodo, todosFilter } = this.props const renderTodoItem = todos.map(todo => { return ( <TodoItem key={todo.id} todoItem={todo} onItemDelete={id => deleteTodo(id)} onUpdateTodo={(id, content) => updateTodo(id, content)} onItemToggleComplete={id => toggleDoneTodo(id)} /> ) }) return ( <List> {todos.length > 0 && <Divider />} {!todos.length && <TodoEmptyState activeFilter={todosFilter} />} {renderTodoItem} </List> ) } render() { return this.renderTodos() } } //filter todos const setFilterTodos = (todos, filter) => { switch (filter) { case SHOW_ALL_TODOS: return todos case SHOW_COMPLETED_TODOS: return todos.filter(t => t.completed) case SHOW_ACTIVE_TODOS: return todos.filter(t => !t.completed) default: return todos } } //return action creators function mapDispatchToProps(dispatch) { return bindActionCreators( { deleteTodo, updateTodo, toggleDoneTodo, }, dispatch ) } //return data our component needs function mapStateToProps({ todos, todosFilter }) { return { todos: setFilterTodos(todos, todosFilter), todosFilter, } } //export TodoComponent export default connect(mapStateToProps, mapDispatchToProps)(Todos)

Todo item

The todoItem.js is a presentational component and we're passing our actions and store via props. This component is 'unaware' of Redux. Its only responsibility is to render the UI and trigger callbacks to the container component.

todoItem.js

import React, { Fragment } from "react" //material-ui components import ListItem from "@material-ui/core/ListItem" import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction" import ListItemText from "@material-ui/core/ListItemText" import IconButton from "@material-ui/core/IconButton" import DeleteOutline from "@material-ui/icons/DeleteOutline" import Checkbox from "@material-ui/core/Checkbox" import Divider from "@material-ui/core/Divider" import Tooltip from "@material-ui/core/Tooltip" import InputBase from "@material-ui/core/InputBase" //receive Redux state via props, we invoke actions via props const TodoItem = ({ todoItem, onItemDelete, onUpdateTodo, onItemToggleComplete, }) => { const handleTextChange = event => { onUpdateTodo(todoItem.id, event.target.value) } return ( <Fragment> <ListItem> <Tooltip title="Completed" placement="right"> <Checkbox checked={todoItem.completed} onChange={() => onItemToggleComplete(todoItem.id)} /> </Tooltip> <ListItemText primary={ <InputBase multiline={true} value={todoItem.content} onChange={event => handleTextChange(event)} /> } /> <ListItemSecondaryAction onClick={() => onItemDelete(todoItem.id)}> <Tooltip title="Delete" placement="right"> <IconButton aria-label="delete todo"> <DeleteOutline /> </IconButton> </Tooltip> </ListItemSecondaryAction> </ListItem> <Divider /> </Fragment> ) } export default TodoItem

Add a todo

Let's make the addTodo a container component and dispatch actions directly without having to use mapDispatchToProps. We do need the connect function.

addTodo.js

import React from "react" import { connect } from "react-redux" //action creator import { addTodo } from "../../redux/actions/todoActions" //material-ui components import Button from "@material-ui/core/Button" import TextField from "@material-ui/core/TextField" //passing dispatch directly const AddTodo = ({ dispatch }) => { let input return ( <form autoComplete="off" onSubmit={e => { e.preventDefault() if (!input.value.trim()) { return } dispatch(addTodo(input.value)) input.value = "" input.style.height = "auto" }} > <TextField id="addTask" inputRef={node => (input = node)} placeholder="Empty task" fullWidth margin="normal" variant="outlined" autoFocus={true} multiline={true} spellCheck="false" InputLabelProps={{ shrink: true, }} /> <Button style={{ float: "right", marginTop: "8px" }} variant="contained" size="medium" color="primary" type="submit" > Add Task </Button> </form> ) } export default connect()(AddTodo)

Filter todos

The filter is a container component. We use connect to access our store and dispatch the filter changes.

filterTodos.js

import React from "react" import { connect } from "react-redux" import { bindActionCreators } from "redux" //constants import { SHOW_ALL_TODOS, SHOW_COMPLETED_TODOS, SHOW_ACTIVE_TODOS, } from "../../redux/constants/constants" //action creator import { filterTodos } from "../../redux/actions/todoActions" //material-ui components import Button from "@material-ui/core/Button" import Menu from "@material-ui/core/Menu" import MenuItem from "@material-ui/core/MenuItem" const FilterTodos = props => { //material-ui menu const [anchorEl, setAnchorEl] = React.useState(null) const open = Boolean(anchorEl) //material-ui event handler function handleClick(event) { setAnchorEl(event.currentTarget) } //material-ui close menu function handleClose() { setAnchorEl(null) } //dispatch filter action and close menu function handleChange(state) { props.filterTodos(state) handleClose() } return ( <div> <Button aria-owns={open ? "fade-menu" : undefined} aria-haspopup="true" size="small" onClick={handleClick} > {props.todosFilter.split("_").join(" ")} </Button> <Menu id="fade-menu" anchorEl={anchorEl} open={open} onClose={handleClose} > <MenuItem onClick={() => handleChange(SHOW_ALL_TODOS)}>All</MenuItem> <MenuItem onClick={() => handleChange(SHOW_ACTIVE_TODOS)}> Active </MenuItem> <MenuItem onClick={() => handleChange(SHOW_COMPLETED_TODOS)}> Completed </MenuItem> </Menu> </div> ) } //return action creators function mapDispatchToProps(dispatch) { return bindActionCreators( { filterTodos, }, dispatch ) } //return data our component needs function mapStateToProps({ todosFilter }) { return { todosFilter, } } export default connect(mapStateToProps, mapDispatchToProps)(FilterTodos)

Empty state

This is a presentational component. We imported it in todos.js and pass the active filter to render the correct message.

todoEmptyState.js

import React, { Fragment } from "react" import { SHOW_ALL_TODOS, SHOW_COMPLETED_TODOS, SHOW_ACTIVE_TODOS, } from "../../redux/constants/constants" const styles = { container: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", height: "150px", color: "#313777", fontFamily: "Montserrat, sans-serif", }, title: { fontSize: "24px", fontWeight: "bold", margin: "15px 0 5px", }, subtitle: { fontSize: "16px", margin: "5px 0", }, } //receive redux activeFilter and show correct message const TodoEmptyState = ({ activeFilter }) => { const renderMessage = () => { switch (activeFilter) { case SHOW_ALL_TODOS: return ( <Fragment> <h2 style={styles.title}>All clear</h2> <p style={styles.subtitle}>Looks like everything's done.</p> </Fragment> ) case SHOW_COMPLETED_TODOS: return ( <Fragment> <h2 style={styles.title}>Been busy?</h2> <p style={styles.subtitle}> You haven't completed any tasks recently. </p> </Fragment> ) case SHOW_ACTIVE_TODOS: return ( <Fragment> <h2 style={styles.title}>Cheers!</h2> <p style={styles.subtitle}> Looks like you're caught up with your tasks. </p> </Fragment> ) } } return <span style={styles.container}>{renderMessage()}</span> } export default TodoEmptyState

Main view

Finally, let's add our container components to App.js.

App.js

import React from "react" import Todos from "./Todos/todos" import AddTodo from "./Todos/addTodo" import FilterTodos from "./Todos/filterTodos" //material-ui grid import Grid from "@material-ui/core/Grid" const styles = { container: { maxWidth: 500, margin: `50px auto`, }, } const App = () => { return ( <div style={styles.container}> <Grid container spacing={24}> <Grid item xs={12}> <FilterTodos /> <AddTodo /> </Grid> <Grid item xs={12}> <Todos /> </Grid> </Grid> </div> ) } export default App

Edit React Redux