DEV Community

mari tang
mari tang

Posted on • Edited on

Redux for Buddies Pt2: writing to state

Where we last left off, we had a general understanding of the Redux loop. We understood that containers provide access to our store, which we selectively use to hydrate our components' props via mapStateToProps.

As a quick refresher, here's what the general loop looks like:

store (state) -defines-> frontend -triggers-> event handler (or other functionality) -sends data/signal (action) to-> reducer -updates-> store (state)

For convenience's sake, we'll take the code we've written and replace the otherComponent we were rendering with a <p> tag that will display our userCount. With that modification, here's our code so far:

import React, { Component } from 'react';
import { connect } from 'react-redux';
const mapStateToProps = store => ({
    users: store.users.userCount // store looks like: {'userCount':5}
})

const mapDispatchToProps = dispatch =>({
    //we'll fill this in and explain it later!
})

class DisplayUsers extends Component{
    constructor(props){
        super(props);
    }
}
render(){
    <p>{this.props.users}</p>
}
export default connect(mapStateToProps, mapDispatchToProps)(DisplayUsers)

Now, to get state updates to work, we need to send data from the frontend. We'll start with that. Remember mapDispatchToProps?

Well, dispatch is what allows our frontend to send data up to the store. Let's imagine that we have a button that we want to click to add 1 to our usercount.

import React, { Component } from 'react';
import { connect } from 'react-redux';
const mapStateToProps = store => ({
    users: store.users.userCount // store looks like: {'userCount':5}
})

const mapDispatchToProps = dispatch =>({
    //we'll fill this in and explain it later!
})

class DisplayUsers extends Component{
    constructor(props){
        super(props);
    }
}
render(){
  <div>
    <p>{this.props.users}</p>
    <button onClick={/* some function */}>add user</button>
  </div>
}

So, in order to mutate our state, we need to dispatch an action to our store. Let's start by talking about what an action is, starting with how they're created.

Action Creators / Actions

An Action Creator is a function. It takes a single argument in, and returns an object which we refer to as an action. An action is just an object that has two properties on it: type and payload.

Here's what an action creator might look like:

import * as types from '../constants/actionTypes'

export const addUser = (payload) =>({
  type: types.ADD_USER,
  payload: payload  
})

As you can see, addUser returns an object that looks like this: {type: types.ADD_USER, payload: /* whatever payload we give it */}, which is the action that it is creating.

So, what is a type? and what data might we want to have in our payload?

Action Types

A type is really just a string that we're importing from our actionTypes file. By convention, the names of our actions are capitalized.

Basically, there are different types of state updates that we would like our reducers to
handle, and our type determines which one to use. We'll get into our reducers a little further down, but, for now, we'll look at what our actionTypes.js file might have inside of it:

export const ADD_USER = 'ADD_USER';

It's really just a string with a few conventions applied to it.

So, as far as why we'd store that string in a variable instead of just typing it in directly, it has to do with the reason that Redux exists in the first place: scale.

At scale, there are a few benefits to using action types. From Dan Abramov's own words:

  • It helps keep the naming consistent because all action types are gathered in a single place.

  • Sometimes you want to see all existing actions before working on a new feature. It may be that the action you need was already added by somebody on the team, but you didn’t know.

  • The list of action types that were added, removed, and changed in a Pull Request helps everyone on the team keep track of scope and implementation of new features.

  • If you make a typo when importing an action constant, you will get undefined. This is much easier to notice than a typo when you wonder why nothing happens when the action is dispatched.

it's total overkill for a project this scale, but so is Redux. That being said, we'll get to our payload

Payload

payload is the other property on our action. This is the actual data that we'll send to our reducers. (if our action needs to provide data at all- increasing our user count by 1 doesn't require any info other than action type).

We'll need to talk about reducers to really understand actions, but we'll talk about how we send actions to our reducers first.

Back To mapDispatchToProps

So, we've got an actionCreator, which returns an action, which has a type and payload. It's called addUser. Let's import it and send it to our store!

import React, { Component } from 'react';
import { connect } from 'react-redux';

import * as actions from '../actions/actions';

const mapStateToProps = store => ({
    users: store.users.userCount 
})

const mapDispatchToProps = dispatch =>({
    addUser: (e)=>dispatch(actions.addUser(e))
})

class DisplayUsers extends Component{
    constructor(props){
        super(props);
    }
}
render(){
  <div>
    <p>{this.props.users}</p>
    <button onClick={this.props.addUser}>add user</button>
  </div>
}

export default connect(mapStateToProps, mapDispatchToProps)(DisplayUsers)

There are four changes we made.

  • we're importing actions, as we defined above.
  • we're adding a property to the object returned by mapDispatchToProps, which is a function that takes in something called dispatch.
  • mapDispatchToProps returns an object whose properties and methods will be made available on our this.props by virtue of the next thing:
  • We use connect to make the properties on mapStateToProps and the methods on mapDispatchToProps available on our this.props by running a function on all of those things and exporting the results.

dispatch and store are provided by Redux, by virtue of the connect method we're importing from it. dispatch ultimately accepts an action, but in this case we're using an action creator to generate that action, providing similar benefits to using constant action types.

Now that we've done all of this, we can finally hit the reducers.

Reducers

In our implementation of Redux, we'll have an index.js file that will serve as an entrypoint for our reducers- that is, we'll import all of our reducers to index and have redux decide which ones to use. We'll also have a usersReducer.js where we'll write our functionality.

index.js will look like this:

import { combineReducers } from 'redux';
import usersReducer from './usersReducer';
const reducers = combineReducers({
  users: usersReducer
})

export default reducers

First, a note about the key name of users that we're using to store usersReducer. If you look at our mapStateToProps, we're grabbing store.users.userCount. This is why/how we shape our store, which is just an object that holds objects that hold the state associated with their respective reducer.

So, what does our usersReducer look like?

usersReducer should be a function that takes in two parameters, one of which is the state contained by our store, and the other of which is an action (such as the ones we've created with our action creators and dispatched to the store by virtue of our dispatch). The body of our usersReducer should essentially be a big switch statement that returns an object, which will comprise the new state of our store, thus completing the process.

Let's write our usersReducer. We'll create some initial state, and have our function fall back to returning it if nothing happens.

import * as types '../constants/actionTypes';
const initialState = {
  userCount: 0
}

const usersReducer = (state = initialState, action) =>{
  switch (action.type){
    default:
      return state;
  }
}

The type of an action will determine what part of the switch gets fired off. Let's add a case for when we receive ADD_USER as our type.

import * as types '../constants/actionTypes';
const initialState = {
  userCount: 0
}

const usersReducer = (state = initialState, action) =>{
  switch (action.type){
    case types.ADD_USER:{
      const userCount = state.userCount + 1;
      return {
        ...state,
        userCount
      }
    }
    default:
      return state;
  }
}

let's break down what we did here.

First, we're creating a const for userCount. We're doing this to keep our code organized, which is more of a factor when we're trying to create a new state that has multiple properties that differ from the old one. Next, we're simply returning a new object that spreads our previous state, then overwrites the old value of userCount with a new one.

We do this partially to merge our old state into our new state, and partially so that we generate an entirely new state object. In generating a new state object, we ensure that React recognizes that we've changed our state and appropriately rerenders its components. (If you need more info on why we do this, I'll write a bit about primitive vs composite data types in JS and link here later)

Anyways, now that we've handled our reducer, that's the end of the whole Redux cycle. We've written to our store!

Let's walk it through

  1. a user clicks "add user"
  2. "add user" fires off a version of addUser which has had dispatch mapped to it by mapDispatchToProps
  3. addUser takes in the action returned by our ADD_USER action creator and triggers our reducers
  4. our reducers find a matching case for ADD_USER, then generates a new state object.
  5. the new state object gets passed back to our frontend components by mapStateToProps

And that's the flow of this particular Redux implementations. There are plenty of other ways to do it, and the value of it is only really seen at scale. Thanks for following along!

Top comments (0)