Skip to content

Instantly share code, notes, and snippets.

@amysimmons
Last active October 21, 2018 19:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save amysimmons/c13395c181e00e867d18846f21faf935 to your computer and use it in GitHub Desktop.
Save amysimmons/c13395c181e00e867d18846f21faf935 to your computer and use it in GitHub Desktop.

Redux

Notes from the Egghead video series Getting Started With Redux

The whole state of your application is represented as a single JS object called the state or the state tree.The state tree is read only.

To change the state you need to dispatch an action. An action is a plain JS object describing the change in a minimal way. At least, the action should contain a property called type with a string value. For example, when a user clicks a plus button, to update the state tree you might dispatch an action with a type INCREMENT.

{
  type: "INCREMENT"
}

If needed, you can add more information to the action, such as an id or index. For example, adding an item to a todo list might look like this:

{
  id: 1,
  text: "learn redux",
  type: "ADD_TODO",
}

Inside any redux app you have to write a function that takes the previous state of the app, the action being dispatched, and returns the next state of the app. The function has to be pure, and does not modify the state given to it. It has to return a new object. It is called the reducer.

If the reducer receives undefined for the state of the app, it should return a default state. If the reducer receives an action that it does not know how to handle, it should return the current state.

A note on pure functions: Pure functions have a return value that depends solely on the function arguments. They do not have any side effects. Pure functions do not modify or mutate the values passed to them. You can avoid array mutations with .concat .splice and …spread. You can avoid object mutations with Object.assign and …spread. Both create new objects, rather than modifying the original.

For example, a reducer for a todo app might look like this:

const todos = (state = [], action) => {
  switch(action.type) {
    case 'ADD_TODO':
      return [
        // use the spread operator to copy all existing state into a new array
        // and add an object for the new todo 
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false,
        }
      ];
     case 'TOGGLE_TODO':
      return state.map(todo => {
        if (todo.id !== action.id) {
          return todo;
        }
        return {
          // use spread operator to return all the properties of the original todo, 
          // but an inverted value of the completed field
          // this does not mutate the original todo, but returns a new object 
          ...todo,
          completed: !todo.completed,
        };
       });
     default: 
       return state;
  }
};

Reducers can call other reducers to delegate and abstract away handling of state updates. There is always a single, top-level reducer managing the state of your app. But it is convenient to express it as many reducers calling each other, each contributing to a part of the state tree. This pattern is called composition. For example, the above reducer code could be refactored so that each case handles the updating of the todos array, but does not handle the todo itself. Instead, it would call another reducer called todo, which handles the creating and updating of a todo. As convention, the new todo reducer would also accept state and action arguments.

For example, the above case statement would now look something like this:

    case 'ADD_TODO':
      return [
        // use the spread operator to copy all existing state into a new array
        // and add an object for the new todo 
        ...state,
        todo(undefined, action),
      ];
     case 'TOGGLE_TODO':
      return state.map(t => todo(t, action));

The reducer composition pattern lets different reducers handle different parts of the state tree and then combine their results. Because this pattern is so common, Redux provides a function called combineReducers. It generates the top level reducer of your app for you. It takes one argument that is an object. The object specifies the mapping between the state field name and the reducer managing that piece of state. The combineReducers call return value is a reducer function.

const { combineReducers } = Redux;
// this combineReducers call says the todos field inside the state object will be updated by the todos reducer
// and the visibilityFilter field in the state object will be updated by calling the visibilityFilter reducer 
// the results will be assembled into a single object 
const todoApp = combineReducers({
  // the key corresponds to the field of the state object that the reducer will manage 
  // the value is the reducer it should call to update the corresponding state field
  todos: todos,
  visibilityFilter: visibilityFilter
});

Redux comes with a function called createStore. The store holds the current app state and lets you dispatch actions. When you create it you specify the reducer which tells how the state is updated with actions. createStore takes the reducer as an argument.

The store itself has getState, dispatch and subscribe methods:

  • getState will get the current state.
  • dispatch is for dispatching actions, it takes the action as an argument.
  • subscribe lets you register a callback that will be called every time an action has been dispatched, so that you can update the UI to reflect the application state. For example, you might pass a render method to subscribe.

Under the hood, the createStore function provided by redux is implemented like this: https://gist.github.com/amysimmons/b335034952451d25209e7cf862022f47

The video series describes several ways that your components can access the store:

  • You can make the store available globally. This does not scale and is not ideal for testing.
  • You can pass the store down explicitly as props, starting from your top level component. This is tiresome as it requires threading the store down through many layers of components.
  • You can pass the store down implicitly via Context. You provide the context containing the store at the top level component, and it can be accessed by all child and grandchild components.
  • You can pass the store down with <Provider> from ReactRedux. To use the component from ReactRedux we added the script tag and imported Provider, wrapped out parent component in Provider, and passed the store to it as props. This removed the need to declare our own Provider class as we did with the Context API.

Lesson 17 and beyond go through creating a Todo App with Redux and React. I coded along and added comments to the code here: https://github.com/amysimmons/redux-react-todo

The lessons make a distinction between presentational and container components in a Redux-React application. Here's what I learnt:

  • Presentational components are responsible for how things look and render, they do not know about behaviour
  • Container components are responsible for the behaviour, such as dispatching actions to the store
  • The job of a container component is to connect the presentational components to the redux store and specify the data and behaviour that the presentation component needs
  • Separating presentational and container components decouples your rendering from redux, so presentational components should not know about redux

Decoupling your presentation and container components is a good thing if later you want to move away from redux and use another framework. The downside is that you end up threading a lot of props from the container components to get them to the leaf presentation components, including callbacks. But the video series says this can be solved by introducing intermediate container components, rather than having one parent container. So when props get passed down several layers, and they're not used in the middle layer, this might be an opportunity to add another container component.

Because all container components are similar they can be auto generated with the ReactRedux connect function.

So this...

class VisibleTodoList extends Component {
  componentDidMount() {
    const {store} = this.context;
    // subscribe lets you register a function that will be called every time
    // an action is dispatched, so that you can update the UI to reflect
    // the new application state.
    this.unsubscribe = store.subscribe(() => {
      this.forceUpdate();
    })
  }

  componentWillUnmount() {
    this.unsubscribe();
  }

  render() {
    const props = this.props;
    const {store} = this.context;
    const state = store.getState();

    return (
      <TodoList
        todos={getVisibleTodos(state.todos, state.visibilityFilter)}
        onTodoClick={
          (id) => {
            store.dispatch({
              type: 'TOGGLE_TODO',
              id,
            });
          }
        }/>
    )
  }
}
VisibleTodoList.contextTypes = {
  store: React.PropTypes.object
}

Gets replaced with this...

// mapStateToProps maps the redux store state to the props of the component.
// These props will be updated any time the state changes.
const mapStateToProps = (state) => {
  return {
    todos: getVisibleTodos(
      state.todos,
      state.visibilityFilter
    )
  }
}

// mapDisptachToProps maps the dispatch method of the store to the callback props of
// the component.
const mapDisptachToProps = (dispatch) => {
  return {
    onTodoClick: (id) => {
      dispatch({
        type: 'TOGGLE_TODO',
        id,
      })
    }
  }
}

// mapStateToProps and mapDisptachToProps describe a container
// component so well, that instead of writing a container from
// scratch it is generated using the connect function provided
// by ReactRedux.
const {connect} = ReactRedux;

// VisibleTodoList is a container component generated by connect.
const VisibleTodoList = connect(
  mapStateToProps,
  mapDisptachToProps
)(TodoList);

The video also explained the concept of action creators, and why it is important to extract them in your code. An action creator takes arguments about the action and returns and object that can be dispatched. They are extracted and all kept in the one place. They are useful for documenting your code, telling your team what kinds of actions the components can dispatch.

For example:

// action creator
const toggleTodo = (id) => {
  return {
      type: 'TOGGLE_TODO',
      id,
  }
}

The video series also touched on the difference between a class and a function component. Function components don't have instances, where class components do. Here's an example that was covered in the video series:

// class component 

// Component is a base class for all react components.
const { Component } = React;

class TodoApp extends Component {
  render() {
    const {todos, visibilityFilter} = this.props;

    return (
      //...
    )
  }
}

// function component 
const TodoApp = ({todos, visibilityFilter}) => {
  return (
    //...
  )
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment