DEV Community

Cover image for What is Redux Anyway? (Part 2)
Daniel Hintz
Daniel Hintz

Posted on • Updated on

What is Redux Anyway? (Part 2)

Wherever you see React, it tends to be paired with something called Redux. But what exactly is this Redux thing all about?

In last week's post, I went through the basic React structure, getting more complex as we went along. By the end, it was clear why we would need something to help us better organize our state. In this Part 2 Post, I'll be explaining what Redux does to simplify state management, how to actually use Redux (aka what are the parts in the below diagram), and walk through last week's roster example using the Redux flow.

Note: this is not meant to be a full React/Redux tutorial, rather it's a high-level overview to explain the concepts involved.

As a reminder, we quickly summarized what Redux is by saying:

Redux provides a Store object, accessible from anywhere in your React app, which contains the state of the entire application, as well as the data flow pattern for how to interact with it.

And here's that diagram outlining the pattern again.

Redux Data Flow

React Data Flow (Image: Tanya Bachuk)

Low let's get down to business.

Our Problem

Recall our roster example from last week where we have multiple related components, contained within multiple containers, all accessing the data contained in state.

Example Roster

Example Roster: darker green and lighter green areas are separate container components

Recall how we handled setting that up in React without Redux. We needed to create a second container component that was a parent of both of our individual container elements. And we discussed how, while this is complicated, it's still manageable, but it can get out of hand very quickly in larger applications.

React Complex Flow

Wouldn't it be great if we could somehow have ALL of our components two-way linked to ALL of our data, rather than worrying about building another new layer every time we need to pass data between related components? That's exactly what Redux is for.

Setting up the Store

The first thing we'll need to do is actually get all of that data out of our individual container elements and pull them into one centralized location. This centralized location is called the store and it lives at the top-level of the app (usually called index.js), making it available everywhere.

Creating a Reducer

So the concept of the store is pretty intuitive, but how do we actually create it? To build our store, we must first set up something called a reducer. The reducer is just a pure function which serves two purposes: first it sets the initial state data for the store, second it allows that state to be changed, in a matter of speaking. I say matter of speaking because the state isn't actually directly mutated, the reducer will instead return a new state each time it is called. A reducer function, in its simplest form, is written like so: function reducer(state, action) {code blocks};

Reducer state argument

The 'state' argument in the reducer will be set in the function declaration to the app's initial state. This is going to cause our reducer function to appear a bit more complicated than what is written above, but rest assured it's the same syntax. Since the state could (and generally does) contain information about many different types of data, you'll usually see the state set up with a nested structure containing arrays of objects, like so:

function playersReducer(
  state = {  // start initial state declaration
    players: [
      {
        id: **player's id**,
        name: **player's name**,
        score: 0 // initially the player will have no points
      },
      {
        id: **player's id**,
        name: **player's name**,
        score: 0
      }
    ]
  }, action) {  // end initial state declaration
  // reducer code blocks
};
Enter fullscreen mode Exit fullscreen mode

We'll discuss the actions a little later, but this is already enough to create the store and provide our entire app with the information about the players.

Creating the Store

Once we have our reducer set up, it's a very simple matter to turn it into our store. We simply need to use the createStore() function in our index component and pass our reducer in as an argument: const store = createStore(playersReducer). Now we have a single store that can be accessed anywhere.

So far on our Redux Diagram, we've gotten this far:
Reducer to Store

Accessing the Store

Having a central store is great, but it doesn't do us much good if we can't access it.

Connecting the Store

Continuing with the React structure of container-to-display, we'll want each of our container components to be able to access the data contained in the store. To do this, we'll need to first connect each component to the store using Redux's connect() function. We do this as part of the component's export statement: export default connect(args)(component);.

Connect: mapStateToProps

This function takes two arguments, the first argument is "mapStateToProps" which is actually another Redux function that pulls specified data out of the store and into the component, making them accessible as props.

const mapStateToProps = state => {
  return {
    players: state.players
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, the array of players is accessible within the component via this.props.players. We could also bring in non-player data this way, regardless of which component we're working with. Say we want to pull in data about the coaches as well now:

const mapStateToProps = state => {
  return {
    players: state.players,
    coaches: state.coaches
  }
}

this.props.coaches
Enter fullscreen mode Exit fullscreen mode

And that right there is the beauty of Redux. All of our data is accessible wherever we need it. This is good place to take a step back and look at what we've accomplished so far. To help visualize our data, here is our now-familiar roster app workflow again, but this time using the Redux global store. Notice how the store wraps around the other container components, rather than existing on a separate level like it was before.

One-Way Flow with Redux

One-Way Flow with Redux

And that gets us one step further on our diagram:
Reducer, Store, UI

Updating the Store

Now we're running into the same one-way flow issue that we discussed last week. How can Redux help us to update the information in the store?

Setting up the Reducer Flow

All changes to state will go through our reducer.

Reducer Action Argument

Recall that our reducer has two arguments and we only talked about the first one? Here's where the second argument, reducer(state, action) comes into the picture. The reducer will take in an action argument, which contains instructions for what updates to the state are needed, and uses a switch statement to return a state with the required updates made.

switch (action.type) {
  case "ADD_PLAYER":
    newPlayer = {**player_details**};
    return [...state, newPlayer];

  case "CHANGE_SCORE":
    let playerIndex = state.findIndex(player => player.id === action.id);  // index in state of the player
    let changedPlayer = state[playerIndex]
    changedPlayer.score = action.score  // change the score
    const newState = [...state]  // copy current state
    newState.splice(playerIndex, 1, changedPlayer)  // replace current player score with new player score
    return newState;  // return the new, updated state

  default:
    return state;
};
Enter fullscreen mode Exit fullscreen mode

Creating Actions

The action is what tells the reducer what to do to the state. An action is simply an object that contains a "type" entry and, optionally, value(s) to pass to the reducer. They are usually created via a pure function called an action creator, which is called (or "dispatched") when an event occurs in our app. They will look similar to this:

export const changeScore = player, score => {  // action creator function
    return {  // return the action
        type: 'CHANGE_SCORE',
        id: player.id,
        score: score
    }
}
Enter fullscreen mode Exit fullscreen mode

Connecting Actions to Components

The final puzzle now is how to get this dispatch flow associated with the container component we want to be able to make changes to state.

Connect: mapDispatchToProps

This is done via the second argument in the Redux connect() function connect(mapStateToProps, *mapDispatchToProps*). Similar to mapStateToProps, mapDispatchToProps assigns our action creators to props so that they can be used in our component.

const mapDispatchToProps = dispatch => {
  return {
    changeScore: (player, score) => dispatch(changeScore(player, score)),
    addPlayer: (player) => dispatch(addPlayer(player))
  }
}
Enter fullscreen mode Exit fullscreen mode

And now we can call the changeScore action from our component by typing this.props.changeScore(selected_player, new_score). Once we do, the data will flow from our component to our reducer via the action, and the reducer will then use the data to update the state in the store, completing our diagram.

Full Redux Flow

Now let's take another step back and look at a visualization of the complete Redux flow:

React Flow with Redux

Full React Data Flow with Redux

The benefits may not be immediately obvious from this smaller example, especially after reading all about the work involved in implementing Redux, but say we wanted to add in that Coach Component now. Compare how this would work with and without Redux:

With Redux Without Redux
Extended Flow with Redux Extended Flow without Redux

That's a pretty big improvement, and of course this is still a very simple example! As we add more and more components, the flow on the right will continue to grow more complex, while the Redux flow on the left will keep the same look and scale smoothly.

Final Workflow

Let's recap. To implement Redux we took the following steps:

Remember that the aim of this article is to show the concepts of Redux, not to be a tutorial for building an app with it, so the below code is not a complete app
  1. Created a reducer function
  2. Defined our initial state in the reducer arguments
  3. Defined what changes we expect to make to state within the reducer
  4. Created a store by passing in the reducer on our index.js
  5. Created action creator functions to dispatch actions to the reducer to change state
  6. Used the Connect() Function to connect the store to each container component in our app

The combined code for this would look similar to this:

// Index.js
const store = createStore(rootReducer, applyMiddleware(thunk));

ReactDOM.render(
  // **React index code**
);



// Reducer
function playersReducer(
  state = {  // start initial state declaration
    players: [
      {
        id: 1,
        name: "John",
        score: 0 // initially the player will have no points
      },
      {
        id: 2,
        name: "Bob",
        score: 0
      }
    ]
  }, action) {  // end initial state declaration
  switch (action.type) {
  case "ADD_PLAYER":
    newPlayer = {id: action.id, name: action.name, score: 0};
    return [...state, newPlayer];

  case "CHANGE_SCORE":
    let playerIndex = state.findIndex(player => player.id === action.id);  // index in state of the player
    let changedPlayer = state[playerIndex]
    changedPlayer.score = action.score  // change the score
    const newState = [...state]  // copy current state
    newState.splice(playerIndex, 1, changedPlayer)  // replace current player score with new player score
    return newState;  // return the new, updated state

  default:
    return state;
  };
};



// Action Creator
export const changeScore = player, score => {  // action creator function
    return {  // return the action
        type: 'CHANGE_SCORE',
        id: player.id,
        score: score
    }
};



// ContainerComponents
class PlayersContainer extends Component {
  // Insert Component Code Here
};

const mapStateToProps = state => {
  return {
    players: state.players
  }
};

const mapDispatchToProps = dispatch => {
  return {
    changeScore: (player, score) => dispatch(changeScore(player, score)),
    addPlayer: (player) => dispatch(addPlayer(player))
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(playersContainer);
Enter fullscreen mode Exit fullscreen mode

And finally, if you're curious what a more complex example of using these frameworks would looks like, feel free to take a look at the code for a shopping site that I built using React/Redux here.

What are your thoughts on using React and Redux? What do you like or not like about them? Let me know in the comments!

Latest comments (0)