In this article we are going to see how to use Redux. The state management tool people love to hate.
I personally like it.
Prerequisites
- Basic knowledge of React.
- Have worked with Hooks.
Source code and demo down below
- view source (example with class components is in a different branch named class_example)
- view demo
What is Redux(Quickly)?
Redux is a state management tool that helps you control and update your applications state more efficiently.
Redux itself is a standalone library which means it's framework agnostic. You can use it with any framework but it's usually used with React.
Why should you use it? Passing props up and down can get nasty if you are dealing with larger applicatons. With Redux all your state lives in a single place, which encourage good React architecture.
Core Concepts
- store: A central place that our state lives. It's created by calling a function.
- reducer: Serves our state to the store and updates the state based on actions.
- actions: Functions that are being dispatched(called) and tell the reducer what to do. They do that by sending action types.
- Provider By wrapping our entire app with the Provider API we can access our store from anywhere in our app.
So the basic flow is:
Actions are being dispatched to the reducer. The reducer listens for the action type within a switch statement. If it doesn't find any match it will return the default(our state). The end result will be passed in a function named createStore to create our store.
Let's start and things will get clearer as we go.
Create your react app and install all of our dependencies.
create-react-app redux-tutorial
npm install redux react-redux
With Classes
We create a components folder with a component called SongList.js
.
An actions folders and a reducers folder as well. In the actions folder we will add two additional files. One songActions.js
which will handle all our actions and a types.js
the we store our actions type names as constants.
In the reducers folder we will add a songReducers.js
file that will handle all our reducers and an index file that will bring all our reducers together and combine them in one. In our case we have just one but we could have many.
Our file structure will look something like this.
src
|
actions
|_ songActions.js
|_ types.js
components
|_ SongList.js
reducers
|_ index.js
|_ songReducers.js
Also add this css in index.css
. Just to make things look a bit better.
/*
index.css
*/
ul {
list-style: none;
max-width: 400px;
margin: 0 auto;
background: #ddd;
padding: 20px;
border-radius: 10px;
}
ul li {
padding: 5px;
margin-bottom: 10px;
background: #fff;
display: flex;
justify-content: space-between;
}
ul li button {
border: 2px solid #ddd;
background: #ddd;
cursor: pointer;
margin-left: 4px;
}
ul > form {
margin-top: 50px;
}
ul > form input[type="text"] {
height: 24px;
padding: 4px;
border: none;
font-size: .9rem;
}
ul > form input[type="submit"] {
padding: 8px;
border: none;
background: #333;
color: #ddd;
font-size: .8rem;
}
First in our App.js
we import our Provider
which will wrap our entire app,the createStore
function that creates our store and allReducers
that is the collection of one or many reducers.
After importing our SongList.js
component we store our apps entire state in a store variable.
//
//App.js
//
import React from 'react'
import './App.css'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import allReducers from './reducers'
import SongList from './components/SongList'
let store = createStore(allReducers);
Then we wrap everything.
. . .
function App() {
return (
<Provider store={store}>
<div className="App">
<h1>Songs(with the help of Redux)</h1>
<SongList />
</div>
</Provider>
);
}
. . .
In our songReducers.js
file we set our initial state and pass it in our reducer function. In the switch statement we are going to listen for an action. If none is provided or called we are going to set it to return the state by default.
//
// songReducers.js
//
const initialState = {
songs: [
{title: 'I love redux'},
{title: 'The redux song'},
{title: 'Run to the redux hill'}
]
}
export default function(state = initialState, action) {
switch(action.type) {
default:
return state;
}
}
In our reducers/index.js
we import all our applications reducers (in our case just one) and pass them to a function named combineReducer
. And it does what the name implies. Combines all of our reducers in one and that is what is passed in the createStore
function in App.js
//
// reducers/index.js
//
import { combineReducers } from 'redux';
import songReducers from './songReducers'
const allReducers = combineReducers({
songs: songReducers
});
export default allReducers;
Now the fun part. Let's bring and consume our state in the SongList.js
component. There are a lot to cover here so bear with me.
We import the connect
function that will wrap our SongList.js
component. With connect
we will actually be able to access our state as props.
connect
takes four optional parameters, but in our case we will use the first two.
mapStateToProps
and mapDispatchToProps
. If we use only one of two the one we don't use should be passed as null.
mapStateToProps provide us with the store. Every time the state changes this function will be called
It takes two parameters. state and ownProps.
With state the function is called when the state changes.
With state and ownProps the function is called both when the state changes and when the current component receives props. In our case we just pass state and set songs with the state.songs that was created by our store.
//
// SongList.js
//
. . .
const mapStateToProps = (state) => ({
songs: state.songs
});
. . .
mapDispatchToProps will provide us with the actions we need to use in our component so we can dispatch them and change our state.
It may be a function or an object. In our case it will be an object of the actions we imported from the songActions.js
.
It will look something like this.
//
// SongList.js
//
import React from 'react'
import { connect } from 'react-redux'
import { actionOne, actionTwo } from '../actions/songActions'
. . .
const mapDispatchToProps = {
actionOne,
actionTwo,
}
export default connect(mapStateToProps, mapDispatchToProps)(SongList);
Or we can destructure.
export default connect(mapStateToProps, { actionOne, actionTwo })(SongList);
Since we don't have any actions yet we pass null.
Later on we will pass all the actions we need.
const mapStateToProps = state => ({
songs: state.songs
});
export default connect(mapStateToProps, null)(SongList);
Now we can access the songs we defined in mapStateToProps
as props in our component.
We destructure it in our render function.
//
// SongList.js
//
import React from 'react'
import { connect } from "react-redux"
class SongList extends React.Component {
render() {
const { songs } = this.props.songs;
return (
<ul>
{songs.map((song, i) => {
return (
<li key={song.title}>
{song.title}
</li>
)
})}
</ul>
);
}
}
const mapStateToProps = state => ({
songs: state.songs
});
export default connect(mapStateToProps, null)(SongList);
Now let's see how can we add new songs, delete songs and update songs as well.
In the code below we add a form. when input changes we call the onChange
function, that sets our local state. On the onSubmit
function we dispatch an action with our newSong as a parameter.
Note: that we start to populate our connect
function with the actions we are using.
//
// SongList.js
//
. . .
import { addSong } from '../actions/songActions'
. . .
constructor(props) {
super(props);
this.state = {
newSong: '',
};
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.remove = this.remove.bind(this);
}
onSubmit(e) {
e.preventDefault();
const addedSong = {
title: this.state.newSong
}
this.props.addSong(addedSong);
this.setState({ newSong: '' });
}
onChange(e) {
this.setState({ [e.target.name]: e.target.value });
}
render() {
const { songs } = this.props.songs;
return (
<ul>
{songs.map((song , i) => {
return (
<li key={song.title}>
{song.title}
</li>
)
})}
<form onSubmit={this.onSubmit}>
<input type="text" name="newSong" onChange={this.onChange} />
<input type="submit" value="Add Song" />
</form>
</ul>
);
}
}
const mapStateToProps = state => ({
songs: state.songs
});
export default connect(mapStateToProps, { addSong })(SongList);
In songActions.js
we create the addSong
function and pass the newSong as payload. Payload is data we pass with the action, second parameter in the switch statement in songReducers.js
. We access it as action.payload.
//
// songActions.js
//
import { ADD_SONG } from './types'
export const addSong = (song) => {
return {
type: ADD_SONG,
payload: song
}
}
Note: It is considered best practice to store the action types as constants in a file named types.js
in the actions folder.
//
// actions/types.js
//
export const ADD_SONG = 'ADD_SONG';
Do this with every additional action typey you add.
Now the songReducers.js
will look like this. The action.payload is the song parameter we passed in our addSong
function.
//
// songReducers.js
//
. . .
export default function(state = initialState, action) {
switch(action.type) {
case ADD_SONG:
return {
songs: [action.payload, ...state.songs]
}
default:
return state;
}
}
. . .
To remove a song we follow the same process.
We create a button. When clicking we call the remove
function with the index of the song as a parameter. Again we dispatch the removeSong
action.
//
// SongList.js
//
. . .
import { addSong, removeSong } from '../actions/songActions'
. . .
remove(i) {
this.props.removeSong(i);
}
render() {
const { songs } = this.props.songs;
return (
<ul>
{songs.map((song , i) => {
return (
<li key={song.title}>
{song.title}
<button onClick={() => this.remove(i)}>Delete</button>
</li>
)
})}
<form onSubmit={this.onSubmit}>
<input type="text" name="newSong" onChange={this.onChange} />
<input type="submit" value="Add Song" />
</form>
</ul>
);
}
}
const mapStateToProps = state => ({
songs: state.songs
});
export default connect(mapStateToProps, { addSong, removeSong })(SongList);
Lastly to update a song we must change a few things. First we will modify our initialState
by adding editing: false
in each of our song object. This will control which song is being edited.
//
// songReducers.js
//
. . .
const initialState = {
songs: [
{title: 'I love redux', editing: false},
{title: 'The redux song', editing: false},
{title: 'Run to the redux hill', editing: false}
]
}
. . .
In our songList.js
component depending if a songs editing state is true or false, we will render a different li.
//
// SongList.js
//
. . .
render() {
const { songs } = this.props.songs;
return (
<ul>
{songs.map((song , i) => {
return (
<Fragment key={song.title}>
{(!song.editing) ? (
<li>
{song.title}
<span>
<button onClick={() => this.remove(i)}>Delete</button>
<button onClick={() => this.edit(i, song.title)}>Edit</button>
</span>
</li>
) : (
<li>
<form>
<input
type="text"
name="currentVal"
value={this.state.currentVal}
onChange={this.updatedVal}
/>
</form>
<span>
<button onClick={() => this.cancel(i)}>Cancel</button>
<button onClick={() => this.update(i)}>Update</button>
</span>
</li>
)}
</Fragment>
)
})}
<form onSubmit={this.onSubmit}>
<input
type="text"
name="newSong"
onChange={this.onChange}
/>
<input type="submit" value="Add Song" />
</form>
</ul>
);
}
. . .
With our new adjustments the whole thing looks like this.
//
// SongList.js
//
import React, { Fragment } from 'react'
import { connect } from 'react-redux'
import {
addSong,
removeSong,
editSong,
updateSong,
cancelEdit
} from '../actions/songActions'
class SongList extends React.Component {
constructor(props) {
super(props);
this.state = {
newSong: '',
currentVal: ''
};
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.remove = this.remove.bind(this);
this.edit = this.edit.bind(this);
this.update = this.update.bind(this);
this.cancel = this.cancel.bind(this);
this.updatedVal = this.updatedVal.bind(this);
}
onSubmit(e) {
e.preventDefault();
const addedSong = {
title: this.state.newSong
}
this.props.addSong(addedSong);
this.setState({ newSong: '' });
}
onChange(e) {
this.setState({ [e.target.name]: e.target.value });
}
updatedVal(e) {
this.setState({ [e.target.name]: e.target.value });
}
remove(i) {
this.props.removeSong(i);
}
edit(i, title) {
this.props.editSong(i);
this.setState({ currentVal: title })
}
update(i) {
this.props.updateSong(this.state.currentVal, i);
this.setState({ currentVal: '' })
}
cancel(i) {
this.props.cancelEdit(i);
}
render() {
const { songs } = this.props.songs;
return (
<ul>
{songs.map((song , i) => {
return (
<Fragment key={song.title}>
{(!song.editing) ? (
<li>
{song.title}
<span>
<button onClick={() => this.remove(i)}>Delete</button>
<button onClick={() => this.edit(i, song.title)}>Edit</button>
</span>
</li>
) : (
<li>
<form>
<input
type="text"
name="currentVal"
value={this.state.currentVal}
onChange={this.updatedVal}
/>
</form>
<span>
<button onClick={() => this.cancel(i)}>Cancel</button>
<button onClick={() => this.update(i)}>Update</button>
</span>
</li>
)}
</Fragment>
)
})}
<form onSubmit={this.onSubmit}>
<input
type="text"
name="newSong"
onChange={this.onChange}
/>
<input type="submit" value="Add Song" />
</form>
</ul>
);
}
}
const mapStateToProps = state => ({
songs: state.songs
});
export default connect(mapStateToProps, {
addSong,
removeSong,
editSong,
updateSong,
cancelEdit
})(SongList);
The songActions.js
looks like this.
//
// songActions.js
//
import {
ADD_SONG,
DELETE_SONG,
EDIT_SONG,
UPDATE_SONG,
CANCEL_EDIT
} from './types'
export const addSong = (song) => {
return {
type: ADD_SONG,
payload: song
}
}
export const removeSong = (index) => {
return {
type: DELETE_SONG,
payload: index
}
}
export const editSong = (index) => {
return {
type: EDIT_SONG,
payload: index
}
}
export const updateSong = (title, index) => {
return {
type: UPDATE_SONG,
title,
index
}
}
export const cancelEdit = (index) => {
return {
type: CANCEL_EDIT,
index
}
}
And the songReducer.js
looks like this.
//
// songReducers.js
//
import {
ADD_SONG,
DELETE_SONG,
EDIT_SONG,
UPDATE_SONG,
CANCEL_EDIT
} from '../actions/types'
const initialState = {
songs: [
{title: 'I love redux', editing: false},
{title: 'The redux song', editing: false},
{title: 'Run to the redux hill', editing: false}
]
}
export default function(state = initialState, action) {
switch(action.type) {
case ADD_SONG:
return {
songs: [action.payload, ...state.songs]
}
case DELETE_SONG:
return {
songs: state.songs.filter((s, i) => i !== action.payload)
}
case EDIT_SONG:
return {
songs: state.songs.map((song, i) =>
i === action.payload
? { ...song, editing: true }
: { ...song, editing: false }
)
}
case UPDATE_SONG:
return {
songs: state.songs.map((song, i) =>
i === action.index
? { ...song, title: action.title, editing: false}
: song
)
}
case CANCEL_EDIT:
return {
songs: state.songs.map((song, i) =>
i === action.index ? { ...song, editing: false } : song
)
}
default:
return state;
}
}
With Hooks
Using Redux with Hooks is way better. It's has fewer boilerplate and I think is easier to work with.
Although it adds a layer of abstraction, if you know the Class way of doing it first, things will stay pretty lean and self-explanatory.
Our songActions.js
and songReducers.js
will look exactly the same. The only difference is in our SongList.js
component.
Instead of connect
we are going to use the useSelector
hook to access parts of the state directly, and useDispatch
to dispatch actions.
useSelector
is somewhat equivalent to mapStateToProps
and useDispatch
is somewhat equivalent to mapDispatchToProps
. They have some differences though which you can check the documentation for details.
//
// SongList.js
//
import React, { Fragment, useState } from 'react'
import { useDispatch, useSelector } from "react-redux"
import {
addSong,
removeSong,
editSong,
updateSong,
cancelEdit
} from '../actions/songActions'
const SongList = () => {
const dispatch = useDispatch()
const [newSong, setNewSong] = useState();
const [currentVal, setCurrentVal] = useState();
const { songs } = useSelector(state => state.songs)
const addNewSong = (e) => {
e.preventDefault();
const addedSong = {
title: newSong
}
if(addedSong.title) {
dispatch(addSong(addedSong))
setNewSong('')
}
}
const remove = (i) => {
dispatch(removeSong(i))
}
const update = (i) => {
dispatch(updateSong(currentVal, i))
setCurrentVal('')
}
const edit = (i, title) => {
dispatch(editSong(i))
setCurrentVal(title)
}
const cancel = (i) => {
dispatch(cancelEdit(i))
}
return (
<ul>
{songs.map((song , i) => {
return (
<Fragment key={song.title}>
{(!song.editing) ? (
<li>
{song.title}
<span>
<button onClick={() => remove(i)}>Delete</button>
<button onClick={() => edit(i, song.title)}>Edit</button>
</span>
</li>
) : (
<li>
<form>
<input type="text" value={currentVal} onChange={e => setCurrentVal(e.target.value)} />
</form>
<span>
<button onClick={() => cancel(i)}>Cancel</button>
<button onClick={() => update(i)}>Update</button>
</span>
</li>
)}
</Fragment>
)
})}
<form onSubmit={addNewSong}>
<input type="text" onChange={e => setNewSong(e.target.value)} />
<input type="submit" value="Add Song" />
</form>
</ul>
)
}
export default SongList
Conclusion
That is pretty much it. Redux can get more complicated but the core concepts are the ones mentioned.
Top comments (2)
Hi, I'm a Redux maintainer. Got a couple quick bits of feedback for you.
First, I'd specifically encourage you to try out our new official Redux Toolkit package. It includes utilities to simplify several common Redux use cases, including store setup, defining reducers, immutable update logic, and even creating entire "slices" of state at once, without writing any action creators or action types by hand:
redux-toolkit.js.org
Second, there's actually at least one bug in your code examples, and RTK actually would have prevented it:
This reducer logic is mutating the existing
song
objects. It does create a newsongs
array, but thesong
object is being mutated. To be a correct immutable update, you'd need to copysong
, likereturn {...song, title: action.title, editing: false}
.RTK includes a mutation checking middleware by default when you use its
configureStore()
function, which would have thrown an error after that mutation to let you know it happened.But, with RTK's use of Immer in its
createReducer
andcreateSlice
functions, that could have simply been:In addition, we would recommend using a "feature folder" or a "ducks" folder structure, as shown in the RTK "Advanced Tutorial" page, rather than a "folder by type" approach.
Hope that helps!
Wow!!! Thanks for the feedback. Very interesting suggestions. I will definitely check out the RTK.
Cheers