- Dispatch an initial action
- Explain how dispatching an initial action gives an initial setup of the store's state
Use the src/reducer.js
file to follow along in this code-along. Open
index.html
and try running dispatch({type: "counter/increment"})
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 "counter/increment":
return { count: state.count + 1 };
default:
return state;
}
}
function dispatch(action) {
state = changeState(state, action);
render();
}
function render() {
const app = document.querySelector("#app");
app.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 "counter/increment"
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" });
Now the number displayed in the DOM starts at 0. And each time we call dispatch, the DOM 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 "counter/increment":
return { count: state.count + 1 };
default:
return state;
}
}
function dispatch(action) {
state = changeState(state, action);
render();
}
function render() {
const app = document.querySelector("#app");
app.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,
JavaScript 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 "counter/increment":
return { count: state.count + 1 };
default:
return state;
}
}
Now notice what happens:
dispatch({ type: "@@INIT" });
// => { count: 0 }
dispatch({ type: "counter/increment" });
// => { count: 1 }
How did that work? Let's take it from the top.
let state;
function changeState(state = { count: 0 }, action) {
switch (action.type) {
case "counter/increment":
return { count: state.count + 1 };
default:
return state;
}
}
function dispatch(action) {
state = changeState(state, action);
render();
}
function render() {
const app = document.querySelector("#app");
app.textContent = state.count;
}
dispatch({ type: "@@INIT" });
At the top of the file, we declare but do not assign a value to our state, so it
starts off undefined. Then at the bottom 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.