Skip to content

Instantly share code, notes, and snippets.

@tmkelly28
Last active July 6, 2022 15:35
Show Gist options
  • Save tmkelly28/db4e9f1aca0971378faed2ce95cb6c89 to your computer and use it in GitHub Desktop.
Save tmkelly28/db4e9f1aca0971378faed2ce95cb6c89 to your computer and use it in GitHub Desktop.

Part 2 - Reducer Functions

Let's talk about the "reducer" function. The "reducer" function is going to be unique to each app. When we invoke createStore, we pass in our "reducer" function as the first argument:

const reducer = function () {} // we'll flesh this out shortly!

function createStore (reducer) { // we pass the reducer in as a first argument!

  let currentState = {};
  let reducer = reducer; // our store will have access to the reducer via closure

  function Store () {}
  Store.prototype.getState = function () {
    return currentState;
  };
  Store.prototype.dispatch = function () {};
  Store.prototype.subscribe = function () {};

  return new Store();
}

Most of the work we do in an app using redux is spent writing the reducer function.

Say we have a toy car that we can operate via a remote control. We can make the car move forward, back, left and right. We could represent the "state" of this car as an x-coordinate and a y-coordinate on a 2D plane.

// let's say our toy car starts at (0, 0)
const remoteControlCarState = {
  x: 0,
  y: 0
};

We can imagine that, when we tell the toy car to go "forward" (via our remote control), the y-coordinate on our toy car's state will increase by one unit, and when we tell it to go "back", the y-coordinate will decrease by one unit. Likewise, when we tell it to go "left", the x-coordinate on the toy car state will decrease, and increase when we tell it to go "right".

A "reducer" function for the remote control car would describe how the remote control car's "state" object changes with each command we give it. We might write out something like this:

const remoteControlCarState = {
  x: 0,
  y: 0
};

function reducer (command) {
  if (command === 'FORWARD') remoteControlCarState.y += 1;
  else if (command === 'BACK') remoteControlCarState.y -= 1;
  else if (command === 'LEFT') remoteControlCarState.x -= 1;
  else if (command === 'RIGHT') remoteControlCarState.x += 1;
}

reducer("FORWARD");
console.log(remoteControlCarState); // { y: 1, x: 0 }
reducer("RIGHT");
console.log(remoteControlCarState); // { y: 1, x: 1 }

It works! However, there's a problem - when we receive each command, we are mutating the state object (that is, we're directly changing the same object in memory). Say that someone enters a series of commands, and the car ends up in an unexpected place somehow. We want to figure out how it got there. However, we have no record of how the car's state changed over time. We would have to guess what combination of commands caused the car to get out of whack, and try to reproduce the problem ourselves. It would be MUCH easier if we could just look back and see how the car's state changed every time it got a command.

This is why, instead of mutating the state object, we should make it so that our reducer function gives us a new object each time. Every time we make a change, the reducer gives out a new object representing our new state. We can refactor our previous function to work like this now:

const initialRemoteControlCarState = {
  x: 0,
  y: 0
};

// our reducer function will now take a command, AND the previous state object
function reducer (previousState, command) {
  
  // we can cleverly use Object.assign to make a copy of our previous state
  const newState = Object.assign({}, previousState);

  if (command === 'FORWARD') newState.y += 1;
  else if (command === 'BACK') newState.y -= 1;
  else if (command === 'LEFT') newState.x -= 1;
  else if (command === 'RIGHT') newState.x += 1;

  return newState; // now we return the new state object
}

const state1 = reducer(initialRemoteControlCarState, "FORWARD");
console.log(state1); // { y: 1, x: 0}
const state2 = reducer(state1, "RIGHT");
console.log(state2); // { y: 1, x: 1 }

This is nice because now we have our record of changes - this will be much easier to debug! This is a fairly small example, so you still may not understand the benefit of doing this. However, once you start writing larger and larger applications, you'll start to realize the advantages - please just humor us for now.

There are only a couple of differences between the reducer functions we'll write and the one we wrote above:

  1. What we've been calling "commands" are called "actions" by the Redux community.
  2. "Actions" are usually not just string literals (like "FORWARD" or "RIGHT"), but objects that have a "type" property, and that "type" property has the name "FORWARD" or "RIGHT", etc. This is because actions might have more information than just their type.
  3. Instead of using if...else if...else, many folks in the Redux community use the switch statement
  4. We usually make our initial state the "default" value for the first argument (previousState). This way, if we don't have a previousState yet (that is, we're invoking the reducer for the first time), it will give us back our initial state!

Note this is all by convention - you could write it differently and name things differently and still have it work, but it would be confusing for other people in the Redux community.

// command is called action
// also, if someone invokes the reducer without a previousState, this will give us our initial state back!
function reducer (previousState = { y: 0, x: 0}, action) {
  
  const newState = Object.assign({}, previousState);

  // we often use switch instead of if...else - many find it easier to read, because there are fewer curly braces
  switch (action.type) { // actions always have a property called type, which contains the name of the action
    case "FORWARD":
      newState.y += 1;
      break;
    case: "BACK":
      newState.y -= 1;
      break;
    case "LEFT":
      newState.x -= 1;
      break;
    case "RIGHT":
      newState.y += 1;
      break;

    return newState;
  }
}

const initialToyCarState = reducer(undefined, {});
console.log(initialToyCarState) // { y: 0, x: 0 }
const state1 = reducer(initialState, { type: "FORWARD" });
console.log(state1); // { y: 1, x: 0}
const state2 = reducer(state1, { type: "RIGHT" });
console.log(state2); // { y: 1, x: 1 }

And that's how to write a basic reducer function! To summarize, here are the rules for writing a reducer function:

  1. It must take a (previous) "state" object as its first argument
  2. It must take an "action" object as its second argument
  3. It must return a new "state" object each time
  4. It must never mutate the previous state object

Now that we have an idea of what reducer functions do and how to write them, let's see what the store does with it, in the next section.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment