A predictable state container for JavaScript apps

Redux Facts

  • Redux !== React
    Redux is a standalone library.
    Redux is framework agnostic.

     
  • Redux !== Flux
    Flux is an architectural pattern.
    Redux is an implementation.
     
  • Don't believe the hype, Redux is simple
    It's 2KB in size and has a tiny surface API. Let me prove it to you.
     
  • Redux can scale with the complexity of your app

A Functional Approach

What are the advertised benefits to a functional approach?

  • Taming side effects
  • Reducing state changes
  • Greater predictability
  • Easier extensibility
  • Improved testability
  • Minimizing moving parts

PluralSight - Functional Programming with C# (1.5 hours)

How tiny is Redux?

Breaking down "reducers"


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

  add(5, 5);

  //returns 10

Breaking down "reducers"

  var state = { total: 0 };

  function impureAdd(a, b) {
    state.total = a + b;
    return state.total;
  }

  impureAdd(5, 5, state);

  //returns 10

Breaking down "reducers"


  function calc(a, b, command) {
    if(command === "ADD") {
      return a + b;
    } else if(command === "SUBTRACT") {
      return a - b;
    }
  }

  calc(5, 5, "ADD");

  //returns 10

Breaking down "reducers"


  //This reducer is impure

  function reducer(state, action) {
    switch(action.type) {
      case 'ADD_COUNTER':
        state.total += action.value;
        return state;
    }
  }

  var stateBefore = { total: 5 };
  var action = {type: 'ADD_COUNTER', value: 5 }
  var stateAfter = reducer(stateBefore, action);
  
  console.log(stateAfter.total);
  //returns 10

Breaking down "reducers"


  //This reducer is pure

  function reducer(state, action) {
    switch(action.type) {
      case 'ADD_COUNTER':
        var newState = {};
        newState.total = state.total + action.value;
        return newState;
    }
  }

  var stateBefore = { total: 5 };
  var action = {type: 'ADD_COUNTER', value: 5 }
  var stateAfter = reducer(stateBefore, action);
  
  console.log(stateAfter.total);
  //returns 10

Breaking down "reducers"


  //ES6 Object.assign
  function reducer(state, action) {
    switch(action.type) {
      case 'ADD_COUNTER':
        var newState = Object.assign({}, state);
        newState.total = state.total + action.value;
        return newState;
    }
  }

  var stateBefore = { total: 5 };
  var action = {type: 'ADD_COUNTER', value: 5 }
  var stateAfter = reducer(stateBefore, action);
  
  console.log(stateAfter.total);
  //returns 10

Breaking down "reducers"


 //ES next object spread
 function reducer(state, action) {
   switch(action.type) {
     case 'ADD_COUNTER':
       return {...state, total: state.total + action.value };
   }
 }

 var stateBefore = { total: 5 };
 var action = {type: 'ADD_COUNTER', value: 5 }
 var stateAfter = reducer(stateBefore, action);
  
 console.log(stateAfter.total);
 //returns 10

Breaking down "reducers"


 // Bring in Redux!
 var createStore = require('redux').createStore;
 // Define a reducer
 function reducer(state, action) {
   if(!action.type) { return state; }
   switch(action.type) {
     case 'ADD_COUNTER':
       return {...state, total: state.total + action.value };
     default:
       return state;
   }
 }

 var store = createStore(reducer, { total: 5 });
 store.dispatch({type: 'ADD_COUNTER', value: 5 }); //10
 store.dispatch({type: 'ADD_COUNTER', value: 5 }); //15
 console.log(store.getState()); //{ total: 15 }

Breaking down "reducers"


 // Bring in Redux!
 import { createStore } from 'redux';
 // Define a reducer
 function reducer(state, action) {
   if(!action.type) { return state; }
   switch(action.type) {
     case 'ADD_COUNTER':
       return {...state, total: state.total + action.value };
     default:
       return state;
   }
 }

 const store = createStore(reducer, { total: 5 });
 store.subscribe(function() {
   console.log('Value changed!', store.getState());
 });
 store.dispatch({type: 'ADD_COUNTER', value: 5 });

Reducer Facts

  • Reducers must be synchronous
     
  • Reducers must not dispatch actions
     
  • Reducers should be pure
     
  • Reducers are deterministic

A Sample App

Using nothing but Redux and the DOM

How do I async?

// Inside your app

// <button onclick="dispatchAction()">Do Work</button>

function dispatchAction() {
  //Show loading indicator
  store.dispatch({ type: "REQUEST_STARTED" });
  
  //Make AJAX call
  $.ajax({ url: "/foo/bar" })
    .done((data) => {
      //Remove loading indicator, update with new data
      store.dispatch({ type: "REQUEST_COMPLETED", value: data });
    });
}

How about validation?

function dispatchAction() {
  //Show loading indicator
  store.dispatch({ type: "REQUEST_STARTED" });
  //Perform synchronous validation
  store.dispatch({ type: "VALIDATE_FORM" });  
  if(!store.getState().isValid) { return; }
  //Make AJAX call
  $.ajax({ url: "/foo/bar" })
    .done((data) => {
      //Remove loading indicator, update with new data
      store.dispatch({ type: "REQUEST_COMPLETED", value: data });
    });
}

//ES next goodness
async function emailChanged(email) {
  store.dispatch({ type: "UPDATE_EMAIL", value: email });
  const emailValid = await $.ajax({ url: "/is/email/valid" });
  store.dispatch({ type: "EMAIL_VALID_RECD", value: emailValid });
}

How Redux Fits Into Your App

"App"

Controller View

Store

State

Child

Component

Child

Component

Child

Component

Reducer(s)

UI components only reflect the state passed to them.

Logic lives in the reducers.

dispatch

subscribe

How to Scale Redux

  • You can break up large reducers into separate modules/files
     
  • You can use multiple reducers via "combineReducers"
    //Reducer 1
    export default function todos(state = [], action) { ... }
    //Reducer 2
    export default function counter(state = 0, action) { ... }
    
    //Combine Reducers
    const combinedReducer = combineReducers({ todos, counter });
    //Create the store
    const store = createStore(combinedReducer);
    
    store.getState(); // returns { todos: [], counter: 0 }
    //Each reducer is handed its own branch of the state tree

Can you tell me what this app does?

    // actionTypes.js
 
    export default {
      ADD_ITEM: "ADD_ITEM",
      REMOVE_ITEM: "REMOVE_ITEM",
      EMPTY_CART: "EMPTY_CART",
      CART_LOADED: "CART_LOADED",
      CREATING_ORDER: "CREATING_ORDER",
      ORDER_CREATED: "ORDER_CREATED"
    }

For Really, Really Big SPAs...

This is for apps that have large teams divided by products that ship sub-apps within a single enclosing "app shell".

Action Creators

    // actions.js
 
    export function addCounter(amount) {
      return { type: actionTypes.ADD_COUNTER, value: amount };
    }

    export function resetCounter() {
      return { type: actionTypes.RESET_COUNTER };
    }

    // app file
    
    import * as actions from './actions';
    //store.dispatch({ type: 'ADD_COUNTER', value: 5 });
    store.dispatch(actions.addCounter(5));

Action Creators help define the shape of your actions

Async with Thunks

  // What is a thunk?
  // In this context it's...
  // 1. a function that returns a function

  function thunk() {
    /* outer function */
    return function() { /* inner function */ };
  }

  // 2. that dispatches actions

  function saveProfile() {

    return function (dispatch, getState) {
      dispatch(actions.savingProfile());
      $.ajax({...})
         .done(() => dispatch(actions.profileSaved()));
    };
  
  }

Async with Thunks

// Inside your app
import * as actions from './actions';
// <button onclick="store.dispatch(actions.doWorkThunk())">...

// inside ./actions
function doWorkThunk() {
  return (dispatch, getState) => {
    //Show loading indicator
    store.dispatch({ type: "REQUEST_STARTED" });
  
    //Make AJAX call
    $.ajax({ url: "/foo/bar" })
      .done((data) => {
        //Remove loading indicator, update with new data
        store.dispatch({ type: "REQUEST_COMPLETED", value: data });
      });
  }
}

Boilerplate and Best Practices

Connect, Mapping Dispatch, Mapping State, Binding Action Creators

Learn More...

More Resources

Questions?

Thanks for your time.

Redux: A Predictable State Container for JavaScript Apps

By Michael Snead

Redux: A Predictable State Container for JavaScript Apps

A presentation on Redux for the Jacksonville node.js meetup group.

  • 1,941