Redux

The story about state

MV* (Angular, Backbone, Ember)

  • Model: manages the behavior and data of the application domain
  • View: represents the display of the model in the UI
  • Controller: takes user input, manipulates the model and causes the view to update

What's wrong?

UI

Filters

Search Results

MV* (Angular, Backbone, Ember)

Sorting

UI

UI

Tracking

Log

Validation

Locale

Tabs

 

Super crazy requirement

 

What's wrong?

MV* (Angular, Backbone, Ember)

  • application state in multiple places
  • application state is not predictable
  • easy to break something by accident because of dependencies
  • hard to track bugs
  • hard to explain application logic to newcomers

Something happened

State changed

UI is rendered

UI is rendered

Something happened

State changed

UI is rendered

UNI-DIRECTIONAL

DATA FLOW

New mindset

Here comes Redux

Application state is:

ONE

SINGLE

JAVASCRIPT

OBJECT

READ-ONLY

Common Redux misconception: state is held in a “giant object”. It’s just object referencing a few other objects. Nothing giant about it.

Dan Abramov (creator of Redux)

How to change the state

CURRENT

STATE

PURE

FUNCTION*

ACTION

NEW STATE

* No side effects

Reducer

const newState = reducer(currentState, action);
// action
{ type: 'ACTION TYPE', /*...*/ }

YOU SHALL NOT MUTATE  THE STATE

TODO app example

// state of simple TODO app
const initialState = {
  visibilityFilter: 'all',
  todos: []
};

function todosReducer(state = initialState, action) {
    switch (action.type) {
      case ADD_TODO:
        const todoItem = {
          text: action.text, 
          completed: false 
        };
        return R.append(todoItem, state.todos);
      default:
        return state;
    }
}

Helper libs:

R.append('tests', ['write', 'more']);
//=> ['write', 'more', 'tests']
R.assoc('c', 3, {a: 1, b: 2});
//=> {a: 1, b: 2, c: 3}

Redux store

import { createStore } from 'redux';

const store = createStore(reducer, initialState);

Store

getState()

subscribe()

dispatch()

subscribe to state changes in UI

get the current state of the app

dispatch an action to reducers

Selectors

{
    products: [
        {
            id: 1,
            title: 'Product 1',
            description: 'Lorem ipsum',
            price: 1000
        },
        {
            id: 2,
            title: 'Product 2',
            description: 'Lorem ipsum',
            price: 2000
        }
    ],
    user: {
        name: 'John Doe',
        role: 'admin',
        currency: 'EUR'
    },
    basket: {
        items: [1, 1, 2],
        totalPrice: 4000
    }
}
        

Reselect

  • Selectors extracts parts of the store
  • Efficient
  • Composable
import { createSelector } from 'reselect'

const productsSelector = state => state.products;
const basketItemsSelector = state => state.basket.items;

const totalProductsSelector = createSelector(
  productsSelector,
  (items) => R.length(items)
)

const basketViewSelector = createSelector(
  productsSelector,
  basketItemsSelector,
  (products, basketItems) => {
     // produce data needed by the view
  }
)

Redux data flow

Action

Reducer

Store

View

<input ng-model="vm.text">
<a ng-click="vm.addTodo(vm.text)">
  add
</a>
<todo-item todo="todo"
    ng-repeat="todo in vm.todos">
</todo-item>
{
  type: 'ADD TODO',
  text: 'Learn Redux'
}


(state, action) => newState
{
  visibilityFilter: 'all',
  todos: ['Learn Redux']
}

 Learn Redux

add

add

Learn Redux

Why Redux?

  • leverages functional programming principles
  • single source of truth (store)
  • easy to reason about the application state
  • easy to test
  • you can use it with vanilla JS, Angular, React or whatever
  • very small footprint and API surface (228 Bytes min+gzip)

Time Traveling

+

Hot Module Reloading

Connecting UI to Redux

import ...
 
angular.module('app', ['ngRedux', 'app.todos'])
    .config(($ngReduxProvider) => {
        const rootReducer = combineReducers({todos, user});
        $ngReduxProvider.createStoreWith(rootReducer);
      });

angular.module('app.todos', ['ngRedux'])
    .controller('TodosCtrl', ($ngRedux, $scope) => {

        let unsubscribe = $ngRedux.connect(sliceOfTheState, availableActions)(this);
        $scope.$on('$destroy', unsubscribe);
      });

Redux bindings:

How to start with Redux

  1. Design your store
  2. List your actions
  3. Create reducers
  4. Connect UI to store
{
    products: [
        {
            id: 1,
            title: 'Product 1',
            description: 'Lorem ipsum',
            price: 1000
        },
        {
            id: 2,
            title: 'Product 2',
            description: 'Lorem ipsum',
            price: 2000
        }
    ],
    user: {
        name: 'John Doe',
        role: 'admin',
        currency: 'EUR'
    },
    basket: {
        items: [1, 1, 2],
        totalPrice: 4000
    }
}
function addItemToBasket(itemId) {
    return {
        type: 'ADD_ITEM_TO_BASKET',
        id: itemId
    }
}
// basket reducer
function basket(state = initialState, action) {
    switch (action.type) {
      case 'ADD_ITEM_TO_BASKET':
        return R.append(
            action.id,
            state.basket.items
        );
      ...
      default:
        return state;
    }
}
<div class="product" ng-repeat="product in vm.products">
    <product-details item="item"></product-details>
    <a ng-click="vm.addItemToBasket(product.id)">
        add to basket
    </a>
</div>
  1. Async Actions?
  2. "Smart" Thunk actions?
  3. Logging?

Middlewares

import { createStore, applyMiddleware } from ‘redux’;

import rootReducer from ‘../reducers’;

import loggerMiddleware from ‘logger’;

const createStoreWithMiddleware = 
  applyMiddleware(loggerMiddleware)(createStore);

export default function configureStore(initialState) {
  return createStoreWithMiddleware(rootReducer, initialState);
}

const store = configureStore();

Case study: Conditional action

function addItemToBasket(itemId) {
    if ( userIsLoggedIn ) {
        return {
            type: 'ADD_ITEM_TO_BASKET',
            id: itemId
        }
    }
    else {
        return {
            type: 'SHOW_MESSAGE',
            message: 'You have to log in to use basket."
        }
    }
}

Case study: Conditional action

function addItemToBasket(itemId) {
    return (dispatch, getState()) {
        if ( getState().user.isAuthenticated ) {
            dispatch({
                type: 'ADD_ITEM_TO_BASKET',
                id: itemId
            });
        }
        else {
            dispatch({
                type: 'SHOW_MESSAGE',
                message: 'You have to log in to use basket."
            });
        }
    }
}
const thunkMiddleware = ({ dispatch, getState }) =>
  next => action =>
    typeof action === 'function' ?
      action(dispatch, getState) :
      next(action);

Case study: API Calls

function logIn(username, password) {
    return {
        type: ['LOGIN_SUCCESS', 'LOGIN_REQUEST', 'LOGIN_FAILURE'],
        promise: fetch('/api/login', {
            method: 'post',
            body: JSON.stringify({username, password})
        })
    };
}
const promiseMiddleware = store => next => action => {
  const { promise, type: [SUCCESS, REQUEST, FAILURE], ...rest } = action;

  if (!promise) return next(action);

  next({ ...rest, type: REQUEST });
  return promise
    .then(response => {
      store.dispatch({ ...rest, response, type: SUCCESS });
      return true;
    })
    .catch(error => {
      store.dispatch({ ...rest, error, type: FAILURE });
      console.error(error);
      return false;
    });
};

Case study: Tracking

const trackingMiddleware = () => next => action => {

    if(action.type === 'BUY_ITEM_CLICKED') {
     myAnalyticsProvider.track('item bought', action.item.id);
    }

    return next(action);
};

Questions?

Redux++

By Pawel Grabarz

Redux++

The story about state

  • 1,115