Skip to content

Instantly share code, notes, and snippets.

@diegoconcha
Last active January 18, 2022 13:23
Show Gist options
  • Save diegoconcha/8918294bb9df69876b22 to your computer and use it in GitHub Desktop.
Save diegoconcha/8918294bb9df69876b22 to your computer and use it in GitHub Desktop.
Redux Egghead.io Notes

###Redux Egghead Video Notes###

####Introduction:#### Managing state in an application is critical, and is often done haphazardly. Redux provides a state container for JavaScript applications that will help your applications behave consistently.

Redux is an evolution of the ideas presented by Facebook's Flux, avoiding the complexity found in Flux by looking to how applications are built with the Elm language.

####1st principle of Redux:#### Everything that changes in your application including the data and ui options is contained in a single object called the state tree

####2nd principle of Redux:#### The state tree is read-only. Dispatch an action to change the state. An action is a plain javascript object describing the change in as minimal detail as necessary to enact that change.

The state is the minimal representation of the data in your app. The action is the minimal representation of the change to that data.

The structure of the action is up to you but it at a minimum it must have a type property that is defined. Use strings for the action type because they're serializable.

Data gets into the redux application (entered through user input or network operations) through actions

####Pure vs Impure Functions:#### Pure function’s return values only depend on the value of their arguments. They also do not have any side effects such as network or database calls. Call them with the same arguments and you always get the same return value. They are predictable. Also pure functions do not modify the values passed to them.

Impure functions may call the database or network. They may have side effects. They may operate on the dom. They may overwrite the values passed to them.

Some of the functions you write in Redux have to be pure and you need to be mindful of that.

You may have heard that the UI is most predictable when it’s represented as a pure function of the application state. Redux compliments this approach with another idea that state mutations are described as a pure function that takes the current state and the action being dispatched and returns the next state. It’s critical that it does not modify the state being passed to it but rather returns a new object.

####3rd principle of Redux:#### To describe state mutations you write a pure function that takes the previous state and the action dispatched and returns the new state. This function is called the reducer.

####The Reducer:#### The reducer is a pure function that takes the previous state and the actions being dispatched and returns the next state of the application.

The reducer should handle the case where it gets an action that it doesn’t expect by returning the state. It should also handle being passed an undefined state by returning the default initial state.

The reducer for a simple counter (written in ES6) is:

const counter = (state = 0, action) => {
  switch (action.type) {
  case ‘INCREMENT’:
    return state + 1;
  case ‘DECREMENT’:
    return state - 1;
  default:
    return state;
  }
};

Written in ES5:

function counter (state , action) {
  if (typeof state === ‘undefined’) {
    return 0;
  }

  switch (action.type) {
  case ‘INCREMENT’:
    return state + 1;
  case ‘DECREMENT’:
    return state - 1;
  default:
    return state;
  }
}

const keyword in ES6 is block scoped just like let. It prevents reassignment and redeclaration. However, if an object is a const it’s keys can be modified and more keys can be added to it.

####Store:#### The store binds together the three principles of Redux. It holds the current application state object, it lets you dispatch actions, and when you create it you specify the reducer that specifies how state is updated with actions.

To create a store do:

const { createStore } = Redux;
// var createStore = Redux.createStore;

const store = createStore(counter);
// var store = createStore(counter);

The store has three important methods: The first, getState, retrieves the current state of the Redux store.

The second and most commonly used store method, dispatch, allows you to dispatch actions that change the state of your application.

The third method, subscribe, lets you register a callback that the store will call any time an action is dispatched so that you can update the UI of your application to reflect the current state.

Typically we extract the logic that updates the UI from the subscribe callback into a render function which you can pass to subscribe as it’s callback but also which you can call initially to show the initial state.

So now for the simple counter we have:

const render () => {
  document.body.innerText = store.getState();
};

store.subscribe(render); 

render(); // render the initial state

// on click dispatch an increment action
document.addEventListener(‘click’, () => {
  store.dispatch({type: ‘INCREMENT’});
});

####Rewrite the createStore method:#### We can rewrite the createStore method in order to better understand it.

const createStore = (reducer) => {
  let state; // the state is not a const but rather a local variable that can be reassigned
  let listeners = []; // keep track of all the change listeners because the subscribe function can be called many times, each with a unique listener

  const getState = () => state;

  const dispatch = (action) => {
    state = reducer(state, action); // generate the new state
    listeners.forEach(listener => listener()); // call each listener
  };

  const subscribe = (listener) => {
    listeners.push(listener); // register each listener for each call of subscribe
    return () => {
      listeners = listeners.filter(l => l != listener);
    }; // return a function for removing the listener from the listeners array by filtering it out
  };

  dispatch({}); // call dispatch in order to generate the initial state

  return { getState, dispatch, subscribe }; // return the store object
};

####Actions should not mutate state:#### You can use the expect library to write test assertions and the deepFreeze library to prevent mutations to ensure your actions don’t mutate state as you’re writing them.

Use the concat array method to avoid array mutations:

const addCounter = (list) => {
  return list.concat([0]);
};

Or use the ES6 spread operator to do the same more concisely:

const addCounter = (list) => {
  return […list, 0]; // spread the values of list and return a new array with 0 appended at the end
};

Use the ES6 spread operator again to write the removeCounter function concisely while preventing mutations on the list.

const removeCounter = (list, index) => {
  return [
    …list.slice(0, index),
    …list.slice(index + 1)
  ];
};

Now let's write the incrementCounter method:

const incrementCounter = (list, index) => {
  return [
    …list.slice(0, index),
    list[index] + 1,
    …list.slice(index + 1)
   ];
};

Use deepFreeze in your tests to prevent an object being passed into a function from being mutated.

Use Object.assign() from ES6 to assign the properties from an existing object to a new object. In the future we will be able to use the ES7 Object Spread Operator.

const toggleTodo = (todo) => {
  return Object.assign({}, todo, {
    completed: !todo.completed
  });
};

Note that when multiple objects have the same properties, the last one passed to assign wins.

####TodoList reducer:####

const todos = (state = [], action) => {
  switch (action.type) {
  case ‘ADD_TODO’:
    return [
      …state,
      {
        id: action.id,
        text: action.text,
        completed: false
      }
    ];
  case ‘TOGGLE_TODO’:
    // map returns a new array
    return state.map(todo => {
      if (todo.id !== action.id) {
        return todo;
      }

      // use the object spread operator from ES7 to recompose the modified todo
      return {
        …todo,
        completed: !todo.completed
      };
    }); 
  default:
    return state;
  }
};

####Compose Reducers:#### You should compose reducers so they act on parts of the state that they are concerned with. Therefore one reducer calls another.

In the todoList example let's create a todo reducer:

const todo = (state, action) => {
  switch (action.type) {
  case ‘ADD_TODO’:
    return {
      id: action.id,
      text: action.text,
      completed: false
    };
  case ’TOGGLE_TODO’:
    if (state.id !== action.id) {
      return state;
    }

    return {
      …state,
      completed: !state.completed
    };
  default:
    return state;
  }
};

Now the todos reducer (our top-most reducer) calls the todo reducer:

const todos = (state = [], action) => {
  switch (action.type) {
  case ‘ADD_TODO’:
    return [
      …state,
      todo(undefined, action);
    ];
  case ‘TOGGLE_TODO’:
    // map returns a new array
    return state.map(t => todo(t, action); 
  default:
    return state;
  }
};

Now how would we add another property for specifying which todos are visible?

Let's create a reducer for it:

const visibilityFilter = (
  state = ‘SHOW_ALL’,
  action
) => {
  switch (action.type) {
  case ‘SET_VISIBILITY_FILTER’:
    return action.filter;
  default:
    return state;
  }
};

Let's also create a new top-most reducer that calls the visibilityFilter reducer and the todos reducer with the parts of the state that they manage and combines the result into the new state object.

const todoApp = (state = {}, action) => {
  return {
    todos: todos(state.todos, action),
    visibilityFilter: visibilityFilter(state.visibilityFilter, action)
  };
};

####Reducer Composition with combineReducers:#### combineReducers takes an object which maps the state properties with the reducers managing them in order to generate the top-most reducer for you.

Let’s apply it to our todoList example:

const { combineReducers } = Redux;

const todoApp = combineReducers({
  todos: todos,
  visibilityFilter: visibilityFilter
});

####Rewrite combineReducers:####

// reducers is an object of reducers
const combineReducers = (reducers) => {
  // combineReducers returns a reducer
  return (state = {}, action) => {
    return Object.keys(reducers).reduce(
      // accumulate the state over every reducer key and call the corresponding reducer
      (nextState, key) => {
        nextState[key] = reducers[key](
          state[key],
          action
        );
        return nextState;
      },
      {} // initial nextState before any of the keys are processed
    );
  };
};

####Presentation vs Container Components#### Presentational components (aka dumb components) should only be concerned with how things look and not behaviors. Any click handlers or other behaviors should be passed as properties to the component.

Container components (aka smart components) pass data from the store and specify behaviors that, along with the data, are passed down to presentation components. These container components are the only components that interact with Redux.

In the todoList example the todo-app component is the container component that interacts with the Redux store. It specifies the todo click behavior and passes it to the todo-list presentation component which then passes it to each todo presentation component. The component nesting looks like this:

<todo-app>
  <todo-list todos=“visibleTodos” onTodoClick=“store.dispatch({…})”>
    <todo completed=“” text=“” onClick=“”></todo-list>
</todo-app>

Remember that components need to have a single root element.

Having a clean separation between presentation and container components allows you to easily swap redux with another library or framework.

The downside is that you have to pass down behaviors and data via props down the component tree to the leaf components even when the intermediate components don’t use those props themselves. This breaks encapsulation because the parent components need to know about the data the child components need.

This can be fixed by having intermediate container components. A nice side-effect of having these intermediate container components is that the top level container components can be refactored as dumb/presentation components and therefore do not subscribe to the state.

The intermediate container components subscribe to the store and re-render themselves anytime the state is changed. They also dispatch their actions and grab the state via store.getState. Then they use the pieces of the state that they care about.

####Injecting the store into components#### Relying on a top-level store variable is not sustainable or testable. You pass into components via props but that adds a lot of boilerplate.

It’s easier to pass it explicitly via an Angular service or implicitly via a provider context in React. Unfortunately the provider context acts like global variables in the component tree.

####Generating Component Containers with connect method#### Container components can be generated with the redux bindings library’s connect method which takes two functions (mapStateToProps and mapDispatchToProps). The first function calculates the props to be injected into the component from the state and the second calculates the callback props to be injected into the component using the dispatch method on the store.

The connect function merges the props in both functions along with any props passed to the container component when it is used. Then you call the output of connect function with the presentation component that the container component is meant to wrap. The merged props get injected as props into this presentation component.

If the connect function is not passed either of the functions then it will not subscribe to the store (ie. it will not re-render itself when the state changes) but it will inject the dispatch method as a prop to the component so it can dispatch actions as needed.

The first function, mapStateToProps, takes the state as it’s first argument and the container component’s props as the second argument. mapDispatchToProps takes the store’s dispatch method as the first argument and the container component’s props as the second argument.

####Extracting Action Creators#### Action Creators are functions that generate action objects. They are typically kept separate from reducers and components in large Redux applications. This allows other components to be able to generate the same actions.

In the todoList example the action creators would be:

let nextTodoId = 0;
const addTodo = (text) => {
  return {
    type: ‘ADD_TODO’,
    id: nextTodoId++,
    text
  };
};

const setVisibilityFilter =  (filter) => {
  return {
    type: ‘SET_VISIBILITY_FILTER’,
    filter
  };
};

const toggleTodo = (id) => {
  return {
    type: ‘TOGGLE_TODO’,
    id
  };
};
@tayiorbeii
Copy link

@diegoconcha @thejmazz Please consider contributing to the video-by-video breakdown notes: https://github.com/tayiorbeii/egghead.io_redux_course_notes

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