Here's where we left off:
const reducer = toyCarReducer; // pretend that this is the reducer for the toy car we just wrote!
function createStore (reducer) {
let currentState = {};
let reducer = reducer;
function Store () {}
Store.prototype.getState = function () {
return currentState;
};
Store.prototype.dispatch = function () {};
Store.prototype.subscribe = function () {};
return new Store();
}
const store = createStore(toyCarReducer); // creates a store that uses our toyCarReducer
In the last section, we saw how we can use the reducer function we wrote to generate a new state object. We just needed pass in the previous state and an action. Now that the store
has both our reducer and our currentState. We just need a way to pass actions to our reducer. This is what store.dispatch
does.
const reducer = toyCarReducer;
function createStore (reducer) {
let currentState = {};
let reducer = reducer;
function Store () {}
Store.prototype.getState = function () {
return currentState;
};
Store.prototype.dispatch = function (action) {
currentState = reducer(currentState, action); // invoking store.dispatch "resets" our currentState to be the result of invoking the reducer!
};
Store.prototype.subscribe = function () {};
return new Store();
}
const store = createStore(toyCarReducer);
Also, remember that if we invoke the reducer with undefined
as the first argument, it will give us our initial state! The createStore
function expects you to write reducer functions that do this. As long as we follow the rules for how a reducer function should work, this will cause the first value of "currentState" to be our initial state.
const reducer = toyCarReducer;
function createStore (reducer) {
// our first "currentState" is the result of invoking the reducer with undefined, and a dummy action
let currentState = reducer(undefined, {});
let reducer = reducer;
function Store () {}
Store.prototype.getState = function () {
return currentState;
};
Store.prototype.dispatch = function (action) {
currentState = reducer(currentState, action);
};
Store.prototype.subscribe = function () {};
return new Store();
}
const store = createStore(toyCarReducer);
const initialState = store.getState() // { y: 0, x: 0 }
store.dispatch({ type: "FORWARD" });
const state1 = store.getState() // { y: 1, x: 0 }
When we store.dispatch
an action, we invoke our reducer with the currentState and the action, and reassign currentState to be the result.
This works great! It would be nice to know when the state changes though. That way, if we need to do something every time our state changes (like update our UI), we can deal with it. This is exactly what store.subscribe
is for.
store.subscribe
takes a callback function we want to invoke every time the state is changed. We can make as many "subscriptions" as we like - it stores them in another "private" array.
const reducer = toyCarReducer;
function createStore (reducer) {
let currentState = reducer(undefined, {});
let reducer = reducer;
let listeners = []; // set aside a listeners array that we have access to via closure
function Store () {}
Store.prototype.getState = function () {
return currentState;
};
Store.prototype.dispatch = function (action) {
currentState = reducer(currentState, action);
};
Store.prototype.subscribe = function (callback) { // store.subscribe takes a callback...
listeners.push(callback); // and pushes it in the listeners array
};
return new Store();
}
Whenever we're done changing our state, we want to invoke all of these listeners. We can bake this right into store.dispatch
.
const reducer = toyCarReducer;
function createStore (reducer) {
let currentState = reducer(undefined, {});
let reducer = reducer;
let listeners = [];
function Store () {}
Store.prototype.getState = function () {
return currentState;
};
Store.prototype.dispatch = function (action) {
currentState = reducer(currentState, action); // first we update the state...
listeners.forEach(callback => callback()); // ...then we invoke all the listeners!
};
Store.prototype.subscribe = function (callback) {
listeners.push(callback);
};
return new Store();
}
const store = createStore(toyCarReducer);
// we "subscribe" a callback function
store.subscribe(() => console.log('Hey, the state changed to be: ', store.getState()));
store.dispatch({ type: "FORWARD" }); // we would see "Hey, the state changed to be: { y: 1, x: 0 }" logged to the console
Sometimes we might want to cancel our subscription. To help us to this, store.subscribe
returns a function. When we invoke that function, it will remove the callback from the listeners array. This is similar to how setInterval
gives us an interval id back, which we can then pass to clearInterval
.
const reducer = toyCarReducer;
function createStore (reducer) {
let currentState = reducer(undefined, {});
let reducer = reducer;
let listeners = [];
function Store () {}
Store.prototype.getState = function () {
return currentState;
};
Store.prototype.dispatch = function (action) {
currentState = reducer(currentState, action);
listeners.forEach(callback => callback());
};
Store.prototype.subscribe = function (callback) {
listeners.push(callback);
// store.subscribe now returns a function
return function () {
// all this function does is remove the callback we passed in from the listeners array!
listeners = listeners.filter(cb => cb !== callback);
}
};
return new Store();
}
const store = createStore(toyCarReducer);
// we "subscribe" a callback function, AND store the function we get back in a variable called "unsubscribe"
const unsubscribe = store.subscribe(() => console.log('Hey, the state changed to be: ', store.getState()));
store.dispatch({ type: "FORWARD" }); // we would see "Hey, the state changed to be: { y: 1, x: 0 }" logged to the console
unsubscribe(); // invoking the function we stored in "unsubscribe" remove the subscription!
store.dispatch({ type: "FORWARD" }); // nothing logs to the console this time! We removed that listener!
And that's it! That's all there is to Redux! (Well, okay, the real Redux has a few more features, but this is fundamentally how a Redux store operates).
Here's our final result:
const reducer = toyCarReducer;
function createStore (reducer) {
let currentState = reducer(undefined, {});
let reducer = reducer;
let listeners = [];
function Store () {}
Store.prototype.getState = function () {
return currentState;
};
Store.prototype.dispatch = function (action) {
currentState = reducer(currentState, action);
listeners.forEach(callback => callback());
};
Store.prototype.subscribe = function (callback) {
listeners.push(callback);
return function () {
listeners = listeners.filter(cb => cb !== callback);
}
};
return new Store();
}