Redux

Curso de React.JS

Thinking in react

  1. Start with a mock
  2. Break the UI into a component hierarchy
  3. Build a static version
  4. Identify the minimal (but complete) representation of the UI state
  5. Identify where your state should live
  6. Add inverse data flow

Demo time!

Bad Practice

Best Practice

Flux architecture pattern

Single direction data flow

Inverse data flow

Parts

  • Stores: manage business data and state
  • View: a hierarchical composition of React components
  • Actions: events created from user events that triggered on the View
  • Dispatcher: an event bus for all actions

Redux

Highlights

  • Simpler approach to state management inspired in Flux
  • Implments an Event Emitter holding a single value
  • Helps you write applications that behave consitently, run in different environments (client, server and native) and are easy to test
  • Only 2kB, including dependencies

Constraints

  • Single state tree
  • Actions describe updates
  • Reducers apply updates

Parts

  • Singleton Store: manages state and has a dispatch(action) function

  • Provider: a subscriber to the Store which interfaces with some “View” framework like React or Angular
  • Actions: events created from user events that are created under the Provider
  • Reducers: pure functions from previous state and an action to new state

How to implement Redux

Reducer

(state, action) => state

Reducer

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

Store

import { createStore } from 'redux'

const store = createStore(counter)

Store API

store.subscribe(() =>
  console.log(store.getState())
)

Store API

store.dispatch({ type: 'INCREMENT' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
// 1

Best practices

Flux Standard Actions

{
  type: 'ADD_TODO',
  payload: {
    text: 'Do something.'  
  }
}

Flux Standard Actions

{
  type: 'ADD_TODO',
  payload: new Error(),
  error: true
}

Flux Standard Actions

An action must

  • be a plain JavaScript object
  • have a type property

An action may

  • have an error property
  • have a payload property
  • have a meta property

An action must not

  • include properties other than type, payload, error or meta

Action Types

const ADD_TODO = 'ADD_TODO'
const REMOVE_TODO = 'REMOVE_TODO'

export {
  ADD_TODO,
  REMOVE_TODO
}

Action Creators

import { ADD_TODO, REMOVE_TODO } from '../actionTypes'

const addTodo = (text) => {
  return {
    type: ADD_TODO,
    text
  }
}

Design the state shape

{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true,
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}

Reducers

const reducer = (previousState, action) => newState

Reducers

Things you should never do inside a reducer:

  • Mutate its arguments
  • Perform side effects like API calls and routing transitions
  • Call non-pure functions, e.g. Date.now() or Math.random().

Reducer example

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    default:
      return state
  }
}

Reducer example

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })    
    default:
      return state
  }
}

Reducer example

case TOGGLE_TODO:
  return Object.assign({}, state, {
    todos: state.todos.map((todo, index) => {
      if (index === action.index) {
        return Object.assign({}, todo, {
          completed: !todo.completed
        })
      }
      return todo
    })
  })

Splitting reducers

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

Splitting reducers

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

Splitting reducers

import { combineReducers } from 'redux'
import { visibilityFilter, todos } from './reducers'

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp

Usage with React.JS

Presentational vs Container components


Presentational Components Container Components
Purpose How things look (markup, styles) How things work (data fetching, state updates)
Aware of Redux No Yes
To read data Read data from props Subscribe to Redux state
To change data Invoke callbacks from props Dispatch Redux actions
Are written By hand Usually generated by React Redux

Provider

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

let store = createStore(todoApp)

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

Connect

const MyComponent = () => (
    // ...
)

const mapStateToProps = (state) => state

const mapDispatchToProps = (dispatch) => {
    return {
        onSomethingHappened: dispatch(yourAction())
    }
}

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(MyComponent)

Async actions

redux-thunk

  • Middleware function for Redux
  • Allow to write action creators that return a function instead of an action
  • The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met
  • The inner function receives the store methods dispatch and getState as parameters.

What is a thunk?

A thunk is a function that wraps an expression to delay its evaluation

Thunk

// calculation of 1 + 2 is immediate
// x === 3
let x = 1 + 2;

// calculation of 1 + 2 is delayed
// foo can be called later to perform the calculation
// foo is a thunk!
let foo = () => 1 + 2;

async example

const INCREMENT_COUNTER = 'INCREMENT_COUNTER';

function increment() {
  return {
    type: INCREMENT_COUNTER
  };
}

function incrementAsync() {
  return dispatch => {
    setTimeout(() => {
      // Yay! Can invoke sync or async actions with `dispatch`
      dispatch(increment());
    }, 1000);
  };
}

conditional example

function incrementIfOdd() {
  return (dispatch, getState) => {
    const { counter } = getState();

    if (counter % 2 === 0) {
      return;
    }

    dispatch(increment());
  };
}

Usage

npm install --save redux-thunk
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';

// Note: this API requires redux@>=3.1.0
const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);

Demo

References

Redux

By Daniel de la Cruz Calvo

Redux

Slides de la clase sobre Redux para Escuela IT

  • 1,083