Using Redux with Classes and Hooks

October 15, 2019

13 min read

Updated: August 25, 2020

Update: I highly recommend checking out this article showing you how you can do all this with Redux Toolkit. It simplifies a lot of stuff and removes a lot of the boilerplate.


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

Why 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 applications. 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: 0.9rem;
}

ul > form input[type="submit"] {
  padding: 8px;
  border: none;
  background: #333;
  color: #ddd;
  font-size: 0.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"

// Redux
import { Provider } from "react-redux"
import { createStore } from "redux"
import allReducers from "./reducers"

// Components
import SongList from "./components/SongList"

// Set my store
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.

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)
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,
  }
}
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.


Written by John Raptis.
In love with JavaScript, React and programming fundamentals in general.
Follow me on Twitter

© 2020, John Raptis