In this lesson, you will learn the following:
To follow along in this code-along, use the js/reducer.js
file and update
according to the Readme. Open index.html
and try running dispatch({type: "INCREASE_COUNT"})
in the browser console. You should see a 1
appear on the
otherwise blank page.
Currently, we have built our changeState()
reducer, and the dispatch()
and
render()
functions. Remember that we built the dispatch()
function such that
each time we execute it, we call the render()
function:
let state = {count: 0};
function changeState(state, action){
switch (action.type) {
case 'INCREASE_COUNT':
return {count: state.count + 1}
default:
return state;
}
}
function dispatch(action){
state = changeState(state, action)
render()
}
function render(){
document.body.textContent = state.count
}
Notice that by calling dispatch()
with an action as an argument, we do render
something on the page. We dispatch an action of "INCREASE_COUNT"
and we see
the number 1
in our HTML, but we never see the number zero displayed. One
easy way to fix this is to simply call the render()
function at the bottom of
our JavaScript code, like the previous lesson. We'll choose a different
approach, though, and use the dispatch()
function we already have.
Remember that our dispatch()
function also calls our render()
function. So, if
we dispatch a meaningless action, our reducer will simply return the existing
state (the default
case in our switch
), and then our render()
function will
be called. Let's try it by dispatching an action of type @@INIT
.
If you already have index.html
open in browser, refresh the page
and enter the following into the browser console:
dispatch({ type: '@@INIT' })
Cool, now our HTML starts off at zero. And each time we call dispatch, the HTML is appropriately updated.
Note that we can dispatch an action of any type, so long as it doesn't hit our
switch statement. We dispatch an action of type @@INIT
by convention, but you
could just as well choose something else and get the same result:
dispatch({ type: 'beef' })
The switch
will return whatever state was passed into the changeState()
function. Then render()
will be called and that updated state will get applied
to the DOM.
Now, if we want our page to display 0
when it first loads, we can just add dispatch({ type: '@@INIT' })
at the end of the file.
Now that we've seen a simple fix for setting up the initial render of HTML, let's see if there's a simple fix for setting up our state. Notice that currently we set the initial value of the state at the very first line of our JavaScript with the following:
let state = { count: 0 };
The problem here is that we would prefer to look to our reducer to see how to manage the state. After all, our reducer returns the new state every time we dispatch a new action. Perhaps our reducer can also return our initial state?
Let's begin by simply declaring our state, but not assigning it to equal anything. So, we accordingly change the first line of our JavaScript:
let state;
function changeState(state, action) {
switch (action.type) {
case 'INCREASE_COUNT':
return { count: state.count + 1 }
default:
return state;
}
}
function dispatch(action){
state = changeState(state, action)
render()
}
function render(){
document.body.textContent = state.count
}
dispatch({ type: '@@INIT' })
But, we find that dispatching the action of type @@INIT
gives us an error:
Uncaught TypeError: Cannot read property 'count' of undefined(…)
See that? Our render()
function is breaking because now state starts off as
undefined. When we dispatch our action, it calls the reducer, which passes
through our state whose value is undefined, and then returns the default value
of our switch statement, which is just our undefined state.
What would be really nice is if we could say when you pass a state of
undefined
to our reducer, assign a value to our initial state. Luckily, ES6
allows us to pass default arguments to functions and we can give our changeState()
reducer a default argument to do just that. Let's change our reducer to the
following:
function changeState(state = { count: 0 }, action) {
switch (action.type) {
case 'INCREASE_COUNT':
return { count: state.count + 1 }
default:
return state;
}
}
Now notice what happens:
dispatch({ type: '@@INIT' })
-> { count: 0 }
dispatch({type: 'INCREASE_COUNT'})
-> { count: 1 }
Ok, pretty elegant. How did that work? Let's take it from the top.
let state;
function changeState(state = { count: 0 }, action) {
switch (action.type) {
case 'INCREASE_COUNT':
return { count: state.count + 1 }
default:
return state;
}
}
function dispatch(action){
state = changeState(state, action)
render()
}
function render(){
document.body.textContent = state.count
}
dispatch({type: '@@INIT'})
At the top of the file, we declare but do not assign our state, so it starts off
undefined. Then at the bottom of the file, we dispatch an action of '@@INIT'
.
This calls our dispatch()
function and passes through our initial action.
dispatch()
calls the changeState()
reducer. changeState()
is executed, passing
through two local variables: state and action. action
is defined because we
passed { type: '@@INIT' }
into dispatch. state
is currently undefined, so, with
that initial dispatch we are really calling:
changeState(undefined, { type: '@@INIT' })
Because changeState()
now has a default argument, the state
argument is set to
{ count: 0 }
.
When changeState()
executes, the switch
statement executes the default
case,
returning the value of state
. The code changeState(undefined, { type: '@@INIT' })
returns { count: 0 }
,
In dispatch()
, when the changeState()
reducer returns, dispatch assigns the
return value to state
, thus updating our state to the initial value of
{ count: 0 }
. On the next line, render()
is called, displaying 0
in our HTML.
Essentially, we take advantage of our state starting off as undefined, and never being undefined again. This means the reducer's default argument can be used to set up the initial state and never be used again.
We learned that by dispatching an initial action of type '@@INIT'
we get two
benefits: an initial rendering of the state, and the ability to set our initial
state in our reducer. We set our initial state in our reducer by using a
default argument for the state parameter. Because state is not initially
defined, dispatching an action assigns our state to that default value, and then
sets state as the default.