“Redux –

a practical example”

Illarion Koperski

2017-03-08

@illarionvk

Use Redux

  • Standalone
  • Runs in browser and Node.js
  • Consistent and predictable
  • Minimal API
  • Easy to test

Use Immutable.js

Using immutable data structures allows me to:

  • Simplify reducer code
  • Eliminate worries about accidentally mutating the state
  • Efficiently compute derived states using reselect
  • Store minimal state in Redux
  • Write tests faster

Use Ducks

or Redux Reducer Bundles

A proposal by @erikras to keep reducer/actions pairs in an isolated module

Duck bill dog muzzle image by Oppo

File structure

_browserify
├── redux/
│   ├── middleware/
│   ├── modules/
│   │   ├── metafields.js
│   │   └── reducer.js
│   ├── types/
│   │   └── metafields.js
│   └── create.js
└── index.js

Reducer module

import uuid from 'uuid/v1'
import Immutable from 'immutable'
import assign from 'lodash/assign'

// Actions
import { ADD, REMOVE } from '../types/metafields'

// Reducer
const initialState = Immutable.Map({})
export default function reducer (state = initialState, action = {}) {
  if (action.type === ADD) {
    return state.set(action.payload.id, Immutable.Map(action.payload))
  }
  if (action.type === REMOVE) {
    return state.removeIn([action.payload.id])
  }
  return state
}

// Action creators
export function add (payload = {}) {
  const defaults = { id: uuid(), key: '', title: '' }
  return { type: ADD, payload: assign(defaults, payload) }
}

export function remove (payload = {}) {
  return { type: REMOVE, payload: assign({ id: '' }, payload) }
}

// Calculated view state
export function getViewState (state) { return state }

Action types format

export const ADD = 'app/metafields/ADD'
export const REMOVE = 'app/metafields/REMOVE'

Reducer test

import test from 'tape'
import Immutable from 'immutable'

import reducer from '../../_browserify/redux/modules/metafields'
import { ADD, REMOVE } from '../../_browserify/redux/types/metafields'

const actions = {
  add: {
    type: ADD,
    payload: { id: 'UID', key: 'test_field', title: 'Test field' }
  },
  remove: { type: REMOVE, payload: { id: 'UID' } }
}

test('Metafields reducer', (t) => {
  t.assert(Immutable.is(reducer(undefined, {}), Immutable.Map({})),
    'should return the initial state')

  t.deepEqual(
    reducer(undefined, actions.add).toJS(),
    { 'UID': actions.add.payload },
    'should handle adding a metafield'
  )

  const actual = (() => {
    const initialState = Immutable.fromJS({ 'UID': actions.add.payload })
    return reducer(initialState, actions.remove).toJS()
  })()

  t.deepEqual(actual, {}, 'should handle adding a metafield')
  t.end()
})

Action creator test

import test from 'tape'
import isUUID from 'validator/lib/isUUID'
import get from 'lodash/get'
import { add, remove } from '../../_browserify/redux/modules/metafields'
import { ADD, REMOVE } from '../../_browserify/redux/types/metafields'

const expected = {
  add: {
    type: ADD,
    payload: { id: 'UID', key: 'test_field', title: 'Test field' }
  },
  remove: { type: REMOVE, payload: { id: 'UID' } }
}

test('Metafields actions', (t) => {
  t.deepEqual(add(expected.add.payload), expected.add,
    'should add a metafield')

  t.ok(isUUID(
    get(add({ key: 'test_field', title: 'Test field' }), 'payload.id')),
    'should generate UUID for id field')

  t.deepEqual(remove(expected.remove.payload), expected.remove,
    'should remove a metafield')
  t.end()
})

Use Redux devtools extension

Init store with devtools logging

import { createStore, applyMiddleware } from 'redux'
import Immutable from 'immutable'
import { composeWithDevTools } from 'redux-devtools-extension/logOnly'

import reducer from './modules/reducer'

const composeEnhancers = composeWithDevTools({
  serialize: {
    immutable: Immutable
  }
})

const initialState = Immutable.Map()

export default createStore(reducer, initialState, composeEnhancers(
  // applyMiddleware(...middleware),
  // other store enhancers if any
))

redux-immutable

import { combineReducers } from 'redux-immutable'

import metafields from './metafields'

export default combineReducers({
  metafields: metafields
  // other store reducers
})

Don't use Sagas, use Epics

Epic is a function which takes a stream of actions and returns a stream of actions. Actions in, actions out.

Redux

RxJS

Epics

+

=

Links

See you next month at WarsawJS

Redux — a practical example

By Illarion Koperski

Redux — a practical example

How to kickstart your app development with Redux best practices

  • 944