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 calleddispatch
. -
mapDispatchToProps
returns an object whose properties and methods will be made available on ourthis.props
by virtue of the next thing: - We use
connect
to make the properties onmapStateToProps
and the methods onmapDispatchToProps
available on ourthis.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
- a user clicks "add user"
- "add user" fires off a version of
addUser
which has haddispatch
mapped to it bymapDispatchToProps
-
addUser
takes in theaction
returned by ourADD_USER
action creator and triggers ourreducers
- our
reducers
find a matchingcase
forADD_USER
, then generates a newstate
object. - the new
state
object gets passed back to our frontend components bymapStateToProps
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)