When we last left off, we successfully used our createStore() method, and
were able have our application re-render through the rather confusing
connect()
method and Provider component. Whenever something in
JavaScript is confusing, it is generally helpful to place some debuggers in the
code and poke around. In this lesson we will guide you through that, and giving
you a for your eyes only peak at the sordid underworld of connect()
and
mapStateToProps()
.
Now, we made some changes to the codebase, mainly to help this walkthrough a
little easier to digest. If you open up the file shoppingListItemReducer
the
first thing you'll see is that we added a new branch to our case statement. Our
reducer now responds to the action types 'GET_COUNT_OF_ITEMS', and
'GET_COUNT_OF_USERS'. We did this to make our state slightly more complex.
You'll notice at the top of our shoppingListItemReducer that we added a new
key to our initial state called users, and populated it with an initial string
to represent a user. You can also see that we removed the calls to console.log
in the reducer, as we already have Redux Devtools setup.
The next set of changes comes in the ./src/App.js
where you can see that we
now have added a new button labeled click to change users. It does the same
thing as our other button, but this time calls a callback which dispatches an
action to change the part of the state related to users, instead of that related
to items.
At the bottom of the file, inside the mapStateToProps()
function you can see
we placed a debugger. Ok, now that you're a little better situated, let's start
our exploration.
Remember that we encounter mapStateToProps when using the connect function. In the current codebase, we have the code:
// ./src/App.js
...
connect(mapStateToProps)(App)
Meaning that we want to connect our App component to a slice of the store's
state specified in mapStateToProps()
. Currently our mapStateToProps()
looks like the following:
// ./src/App.js
...
const mapStateToProps = (state) => {
debugger;
return { items: state.items }
}
Yes, we added a debugger to the body of our mapStateToProps()
function. So
now boot up the app and click on the two buttons. You will see that clicking on
the Items Count button renders an update to our App Component, while
clicking on the Users Count button does not. This makes sense: inside our App
component all we do is reference the items count.
Ok, now let's open up our console so that we hit our debugger. If you click on
each of the buttons, you'll see that our debugger gets hit with each action that
we dispatch. So even though we are not updating our App component with
information about users, the mapStateToProps()
function is executed with
each change to the store's state. That's an important point. Say it with me one
more time: the mapStateToProps()
method is executed with each change to the
store's state.
Ok, now the next time we are in the debugger, let's notice that if you type the
word state into the console while inside the mapStateToProps()
method, that
it is the entire state of the store and not just that relevant to the component.
Next question: what is so special about this mapStateToProps()
method that
it is executed each time there is a change in state, and receives the entire
state of the store as its argument? Let's change our code to the following in
src/App.js
// ./src/App.js
...
const vanilla = (milkshake) => {
debugger;
return { items: milkshake.items }
}
export default connect(vanilla)(App);
Essentially, all we did was rename our mapStateToProps()
function to
vanilla(), and rename the argument state
to milkshake
. Refresh the app,
click the button, and notice that no functionality changes: the vanilla function
now is hit every time there is a change in state, and milkshake now represents
our store's state. So in other words, whatever function we pass to the
connect()
function will be called each time the state changes, and the first
argument to that function, whatever its name, will be the state of the store.
We can even shorten mapStateToProps()
down to an anonymous arrow function and pass it directly into connect()
:
export default connect( state => ({ items: state.items }) )(App);
If you've got a complicated amount of state you're mapping to props, stick with the original set up.
So in the previous section we saw that whatever function we pass to connect()
is executed each time there is a change in state, and that the argument that
function is executed with is the entire state of the store. Changing the
function back to mapStateToProps()
, let's pay special attention to the
return value to that function:
// ./src/App.js
...
const mapStateToProps = (state) => {
return { items: state.items }
}
export default connect(mapStateToProps)(App);
This return value, is the value of the props that are added to the App component. Let's see what happens if we change the key in the return value from items to orangePeel.
// ./src/App.js
...
const mapStateToProps = (state) => {
return { orangePeel: state.items }
}
Let's also place a debugger inside of our App component, as the first line underneath the render function, this way we can examine the props of our app component.
// ./src/App.js
...
render() {
debugger;
return (
<div className="App">
<button onClick={() => this.handleOnClickItems()}>
Click to change items count
</button>
<button onClick={() => this.handleOnClickUsers()}>
Click to change user count
</button>
<p> {this.props.items.length}</p>
</div>
);
}
...
If you type in this.props
while inside the render function, you will see that
we now have this.props.orangePeel, which returns our array of numbers. So by
changing the key to the return value in mapStateToProps()
we changed the name
of the prop in App. As a second step, let's change the value to orangePeel
as well:
// ./src/App.js
...
const mapStateToProps = (state) => {
return { orangePeel: ['a', 'b', 'c'] };
};
...
Keeping our debugger in our App component's render function, you can see
that this.props.orangePeel
now returns ['a', 'b', 'c']
. So now when we see
the following code, perhaps we understand it a little better.
// ./src/App.js
...
const mapStateToProps = (state) => {
return { items: state.items }
}
export default connect(mapStateToProps)(App);
We understand that the connect()
function calls the mapStateToProps()
function each time there is a change in state, and that mapStateToProps()
receives state
as its first argument.
We also know that mapStateToProps()
can happily ignore the store's state and
return whatever it likes. We know that connect()
takes whatever the
return value is of the mapStateToProps()
function and passes it to the
component that is in those last set of parentheses (in this case, App).
Because we are taking a part of the store's state and porting it to become props of the relevant component, we say that we are mapping it as props to the component, thus the name mapStateToProps.
By now, you may be thinking, why would Redux choose this whole
mapStateToProps()
technique. Didn't we live a simpler and happier life when we
just passed our store down through each component. Well, maybe, but we do get
some benefits by using this pattern. We'll talk more about the benefits of the
connect()
function later, but for now we can discuss the biggest benefit,
separation of concerns.
Separation of concerns is the big win. Take a look at the App component again:
// ./src/App.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import './App.css';
class App extends Component {
handleOnClickItems() {
this.props.store.dispatch({
type: 'GET_COUNT_OF_ITEMS',
});
}
handleOnClickUsers() {
this.props.store.dispatch({
type: 'GET_COUNT_OF_USERS',
})
}
render() {
debugger;
return (
<div className="App">
<button onClick={() => this.handleOnClickItems()}>
Click to change items count
</button>
<button onClick={() => this.handleOnClickUsers()}>
Click to change user count
</button>
<p> {this.props.items.length}</p>
</div>
);
}
}
const mapStateToProps = (state) => {
// debugger;
return { items: state.items }
}
export default connect(mapStateToProps)(App);
You will notice that if it wasn't for the dispatch method (and in a later lesson
we will remove that as well), our component would have no knowledge of our
store, and thus no knowledge of anything related to Redux. This means that
if someone wanted to take the component and use a different backend, like say
Flux, it could. It also means that because all of our Redux is
separated, if we wanted to add in changes to our application to be mobile
by using React Native. Then our Redux logic would largely stay the
same. So with this pattern, both the view and its state management system are
properly separated, and only connected by that connect()
function.
In this lesson we saw that the connect()
function is used for us to connect
our Redux part of the application to the React part of the application
(we'll see even more of this later). We also see that whatever function we pass
as the first argument to that connect()
function is called each time there is
a change of state, and has access to the entire store's state. The connect()
function then takes the return value from the mapStateToProps()
function and
adds that return value to the props of the component that is passed through in
the last parentheses. We call that component a connected component, because that
component is connected to the store.