MOSH: Redux Tutorial – Learn Redux from Scratch


Uncategorized

Updated Oct 1st, 2022

Watched Mosh Hamedami’s Intro to his “Redux Tutorial – Learn Redux from Scratch” course on YouTube here. This is the first hour of his 6 hour course. Full course was $15 as of 10/1/22.

Redux is based on Facebook’s Flux. Mobx is another state management library alternative to redux. Leverage a store.

The redux dev tools chrome extension is a thing.

Benefits: Predictable state changes, centralized state, Cache or preserve page state, (sort’s hang around). Easily debug with undo/redo (time-travel debugging similar to log rocket). Can use with multiple frontend frameworks

Cons: Complexity and Verbosity. You don’t need to use redux for every project. May not be the right tool for the job.

This video’s starter project has simple “webpack” setup.

Functional Programming

Redux is based on the “functional programming” paradigm. Small functions: more concise, easier to debug, easier to test, more scalable.

Functions are first-class citizens, meaning we can store them in a variable, pass them as an argument to other functions and return from other functions.

Higher order functions: function that takes a function as an argument, or return a function, or both.

function greet(fn) {
  console.log(fn())
}

function sayHello() {
  return function() {
    return "hello world"
  }
}

Composing and Piping

Showed custom “trim, wrapInDiv, and toLowerCase” examples. Use “lodash” utility library to help with having to read from right to left (with pipe from lodash) and lots of parenthesis (with compose from lodash to reduce having to nest function calls):

import {compose, pipe} from "lodash/fp"

const transform = compose(wrapInDiv, toLowerCase, trim)
transform(input)

But to prevent having to read form right to left use “pipe” instead of compose

const transform = pipe(trim, toLowerCase, wrapInDiv)
transform(input)

Currying:

Technique that takes a function taking “n” arguments and converts it to having a single argument.

so take the following code:

function add(a, b) {
  return a + b
}

Instead of separate parameters with commas, we use parenthesis

function add(a) {
  return function(b) {
    return a + b
  }
}
add(1)(5)

and with arrow functions:

const add2 = a => b => a + b

So using this currying concept we refactor the following code:

wrap = (type, str) => `<${type}>${str}</${type}>`

into a curried function:

wrap = type => str => `<${type}>${str}</${type}>`

Pure Functions

A pure function always returns the same value if passed the same parameter. So no random values, no current date/time, no global state (DOM, files, db, etc.), no mutation of parameters.

Benefits of pure functions: self-documenting, easily testable, concurrency, cacheable

Immutability

Goes hand and hand with pure functions. JS is a multiple paradigm language. So you can use functional programming principles but you can still mutate data. Strings are immutable but objects and arrays are mutable.

Why immutability? predictability, faster change detection, concurrency

Cons to immutability: performance, memory overhead

Practice immutability with objects:

const updated = Object.assign({}, person, {name: "Bob", age: 30})
console.log(updated)

A better way is to use the spread operator

const updated = {...person, name: "Bob"}

But these are shallow copies to you need to be careful with nested objects.

In order to do deep copy:

const updated = {
  ...person,
  address: {
    ...person.address,
    city: "New York"
  },
  name: "Bob"
}

Very verbose which is why we have libraries to help with this.

Immutability with arrays: adding with slice method, removing with filter and updating with map.

JavaScript does not prevent object mutations because it is not a pure functional programming language. To get around this we have to work with libraries that offer real immutable data structures, (Immutable, Immer, and Mori). Quick tour of immutable.js library (developed FB) is shown. The package “immer” is preferred by the author, created by the creator of mobx, and strong npmtrends.com. Both are used a lot.

import {produce} from "immer"

What’s beautiful about immer is our code looks familiar to us but our data does not get mutated.

Actually Learning Redux

Store is the single source of truth. Cannot directly modify or mutate the store. So we need to write a function that takes the store as an argument and returns the updated store. Use the spread operator or one of the immutability libraries shown earlier.

This function is often called “reducer” and a lot of people gripe about this. How does it know what property to update? The function takes a second parameter that is called an action. A store can have many slices. You can have a reducer for updating each slice of your store. We dispatch actions.

function reducer (store, action) {
  const updated = {..store}
}

So we have three major building blocks: Action (Event), Store and one or more Reducers (Event Handler).

We take an action and dispatch it to the store which forwards this to the reducer. We don’t act with the reducer directly.

Why do it this way? Pipe metaphor. Same action through the same entry point. We have a central place so we can do things like log every action. Implement undo/redo mechanisms.

Example Application Built: Bug Tracker Application

Major steps: Design the store, define the actions, create a reducer, set up the store.

npm install redux

Design what to keep in the store. List of bugs. id, description resolved. Alternatively we could have two slice: one for bugs and current user.

Define the Actions

An “action” is just a plain JS object that describes what just happened. “ADD_BUG” or “bugAdded” etc. Nice to have a type and payload structure but this is optional.

Create the reducer. Can use if…else (seen below) or a switch statement.


function reducer(state = [], action) {
  if (action.type === "bugAdded") {
    return [
      ...state,
      {
        description: action.payload.description,
        resolved: false
      }
    ]
  }
  
  else if (action.type === "bugRemoved") {
    return state.filter(bug => bug.id !== action.payload.id)
  }
  return state
}

The switch version

export default function reducer(store = [], action) {
  switch (action.type) {
    case "bugAdded":
      return [...]
    case "bugRemoved":
      return state.filter(...)
    default"
      return state 
  }
}

The reducers has to be pure functions and free of side effects. Small function in isolated form.

Creating the store

import {createStore} from "redux" 
import reducer from "./reducer"

export default const store = createStore(reducer)
console.log(store)

Note: we do not call reducer in the “createStore” function just reference it.

Import the store into the main part of your application.

Logging the store the console you will see the store is an object with the following properties: dispatch, subscribe, getState, replaceReducer, Symbol(observable)

We can only “getState” from the store. To set the state we need to dispatch an action.

Subscribing to the store

Unsubcribing to the store: If not visible we should unsubscribe to prevent memory leaks.

The “dispatch()” method

Since we refer to the names of our action both when we define them and call them it is common practice to create a reference file in case the name is ever changed on the future we can update in one place.

Create an “actionTypes.js” file

export const BUG_ADDED = "bugAdded"

Then import with asterisk and an alias

import * as actions from "./actionTypes"
// reference like this:
actions.BUG_REMOVED

Action Creators: dispatching an action is not easy, we have to type the entire structure of the object. We can instead create a function that can create this action object for us. Again, great in case there are future changes we can update the code in one place. And we call call this dispatch action from multiple files. So create an “actions.js” or “actionCreators.js” file:

export function bugAdded(description) {
  return {
    type: actions.BUG_ADDED,
    payload: {
      description: description
    }
  }
}

Then import into the “index.js” file

import {bugAdded} from "/.actions"

store.dispatch(bugAdded("Bug 1"))

And you can refactor above “action creator” code using arrow functions for more concise syntax:

export const bugAdded = description => ({
  type: actions.BUG_ADDED,
    payload: {
      description
    }
})

Exercise to create an action that “resolves a bug.” Think of action first so in “actionTypes” create a “BUG_RESOLVED” constant. Then create an action creator. Then create the new case in the reducer.