Home

Awesome

extensible-duck

extensible-duck is an implementation of the Ducks proposal. With this library you can create reusable and extensible ducks.

Travis build status Code Climate Test Coverage Dependency Status devDependency Status

<!-- MarkdownTOC autolink="true" bracket="round" autoanchor="false" markdown_preview="github" --> <!-- /MarkdownTOC -->

Basic Usage

// widgetsDuck.js

import Duck from 'extensible-duck'

export default new Duck({
  namespace: 'my-app', store: 'widgets',
  types: ['LOAD', 'CREATE', 'UPDATE', 'REMOVE'],
  initialState: {},
  reducer: (state, action, duck) => {
    switch(action.type) {
      // do reducer stuff
      default: return state
    }
  },
  selectors: {
    root: state => state
  },
  creators: (duck) => ({
    loadWidgets:      () => ({ type: duck.types.LOAD }),
    createWidget: widget => ({ type: duck.types.CREATE, widget }),
    updateWidget: widget => ({ type: duck.types.UPDATE, widget }),
    removeWidget: widget => ({ type: duck.types.REMOVE, widget })
  })
})
// reducers.js

import { combineReducers } from 'redux'
import widgetDuck from './widgetDuck'

export default combineReducers({ [widgetDuck.store]: widgetDuck.reducer })

Constructor Arguments

const { namespace, store, types, consts, initialState, creators } = options

NameDescriptionTypeExample
namespaceUsed as a prefix for the typesString'my-app'
storeUsed as a prefix for the types and as a redux state keyString'widgets'
storePathObject path of the store from root infinal redux state. Defaults to the [duck.store] value. Can be used to define duck store location in nested stateString'foo.bar'
typesList of action typesArray[ 'CREATE', 'UPDATE' ]
constsConstants you may need to declareObject of Arrays{ statuses: [ 'LOADING', 'LOADED' ] }
initialStateState passed to the reducer when the state is undefinedAnything{}
reducerAction reducerfunction(state, action, duck)(state, action, duck) => { return state }
creatorsAction creatorsfunction(duck)duck => ({ type: types.CREATE })
sagasAction sagasfunction(duck)duck => ({ fetchData: function* { yield ... }
takesAction takesfunction(duck)duck => ([ takeEvery(types.FETCH, sagas.fetchData) ])
selectorsstate selectorsObject of functions<br>or<br>function(duck){ root: state => state}<br>or<br>duck => ({ root: state => state })

Duck Accessors

Helper functions

Defining the Reducer

While a plain vanilla reducer would be defined by something like this:

function reducer(state={}, action) {
  switch (action.type) {
    // ...
    default:
      return state
  }
}

Here the reducer has two slight differences:

new Duck({
  // ...
  reducer: (state, action, duck) => {
    switch (action.type) {
      // ...
      default:
        return state
    }
  }
})

With the duck argument you can access the types, the constants, etc (see Duck Accessors).

Defining the Creators

While plain vanilla creators would be defined by something like this:

export function createWidget(widget) {
  return { type: CREATE, widget }
}

// Using thunk
export function updateWidget(widget) {
  return dispatch => {
    dispatch({ type: UPDATE, widget })
  }
}

With extensible-duck you define it as an Object of functions:

export default new Duck({
  // ...
  creators: {
    createWidget: widget => ({ type: 'CREATE', widget })

    // Using thunk
    updateWidget: widget => dispatch => {
      dispatch({ type: 'UPDATE', widget })
    }
  }
})

If you need to access any duck attribute, you can define a function that returns the Object of functions:

export default new Duck({
  // ...
  types: [ 'CREATE' ],
  creators: (duck) => ({
    createWidget: widget => ({ type: duck.types.CREATE, widget })
  })
})

Defining the Sagas

While plain vanilla creators would be defined by something like this:

function* fetchData() {
  try{
  	yield put({ type: reducerDuck.types.FETCH_PENDING })
    const payload = yield call(Get, 'data')
    yield put({
      type: reducerDuck.types.FETCH_FULFILLED,
      payload
    })
  } catch(err) {
    yield put({
      type: reducerDuck.types.FETCH_FAILURE,
      err
    })
  }
}

// Defining observer
export default [ takeEvery(reducerDuck.types.FETCH, fetchData) ]

With extensible-duck you define it as an Object of functions accessing any duck attribute:

export default new Duck({
  // ...
  sagas: {
    fetchData: function* (duck) {
    	try{
        yield put({ type: duck.types.FETCH_PENDING })
        const payload = yield call(Get, 'data')
        yield put({
          type: duck.types.FETCH_FULFILLED,
          payload
        })
      } catch(err) {
        yield put({
          type: duck.types.FETCH_FAILURE,
          err
        })
      }
    }
  },
  // Defining observer
  takes: (duck) => ([
  	takeEvery(duck.types.FETCH, duck.sagas.fetchData)
  ])
})

Defining the Initial State

Usually the initial state is declared within the the reducer declaration, just like bellow:

function myReducer(state = {someDefaultValue}, action) {
  // ...
}

With extensible-duck you define it separately:

export default new Duck({
  // ...
  initialState: {someDefaultValue}
})

If you need to access the types or constants, you can define this way:

export default new Duck({
  // ...
  consts: { statuses: ['NEW'] },
  initialState: ({ statuses }) => ({ status: statuses.NEW })
})

Defining the Selectors

Simple selectors:

export default new Duck({
  // ...
  selectors: {
    shopItems:  state => state.shop.items
  }
})

Composed selectors:

export default new Duck({
  // ...
  selectors: {
    shopItems:  state => state.shop.items,
    subtotal: new Duck.Selector(selectors => state =>
      selectors.shopItems(state).reduce((acc, item) => acc + item.value, 0)
    )
  }
})

Using with Reselect:

export default new Duck({
  // ...
  selectors: {
    shopItems:  state => state.shop.items,
    subtotal: new Duck.Selector(selectors =>
      createSelector(
        selectors.shopItems,
        items => items.reduce((acc, item) => acc + item.value, 0)
      )
    )
  }
})

Selectors with duck reference:

export default new Duck({
  // ...
  selectors: (duck) => ({
    shopItems:  state => state.shop.items,
    addedItems: new Duck.Selector(selectors =>
      createSelector(
        selectors.shopItems,
        items => {
          const out = [];
          items.forEach(item => {
            if (-1 === duck.initialState.shop.items.indexOf(item)) {
              out.push(item);
            }
          });
          return out;
        }
      )
    )
  })
})

Defining the Types

export default new Duck({
  namespace: 'my-app', store: 'widgets',
  // ...
  types: [
    'CREATE',   // myDuck.types.CREATE   = "my-app/widgets/CREATE"
    'RETREIVE', // myDuck.types.RETREIVE = "my-app/widgets/RETREIVE"
    'UPDATE',   // myDuck.types.UPDATE   = "my-app/widgets/UPDATE"
    'DELETE',   // myDuck.types.DELETE   = "my-app/widgets/DELETE"
  ]
}

Defining the Constants

export default new Duck({
  // ...
  consts: {
    statuses: ['NEW'], // myDuck.statuses = { NEW: "NEW" }
    fooBar: [
      'FOO',           // myDuck.fooBar.FOO = "FOO"
      'BAR'            // myDuck.fooBar.BAR = "BAR"
    ]
  }
}

Creating Reusable Ducks

This example uses redux-promise-middleware and axios.

// remoteObjDuck.js

import Duck from 'extensible-duck'
import axios from 'axios'

export default function createDuck({ namespace, store, path, initialState={} }) {
  return new Duck({
    namespace, store,

    consts: { statuses: [ 'NEW', 'LOADING', 'READY', 'SAVING', 'SAVED' ] },

    types: [
      'UPDATE',
      'FETCH', 'FETCH_PENDING',  'FETCH_FULFILLED',
      'POST',  'POST_PENDING',   'POST_FULFILLED',
    ],

    reducer: (state, action, { types, statuses, initialState }) => {
      switch(action.type) {
        case types.UPDATE:
          return { ...state, obj: { ...state.obj, ...action.payload } }
        case types.FETCH_PENDING:
          return { ...state, status: statuses.LOADING }
        case types.FETCH_FULFILLED:
          return { ...state, obj: action.payload.data, status: statuses.READY }
        case types.POST_PENDING:
        case types.PATCH_PENDING:
          return { ...state, status: statuses.SAVING }
        case types.POST_FULFILLED:
        case types.PATCH_FULFILLED:
          return { ...state, status: statuses.SAVED }
        default:
          return state
      }
    },

    creators: ({ types }) => ({
      update: (fields) => ({ type: types.UPDATE, payload: fields }),
      get:        (id) => ({ type: types.FETCH, payload: axios.get(`${path}/${id}`),
      post:         () => ({ type: types.POST, payload: axios.post(path, obj) }),
      patch:        () => ({ type: types.PATCH, payload: axios.patch(`${path}/${id}`, obj) })
    }),

    initialState: ({ statuses }) => ({ obj: initialState || {}, status: statuses.NEW, entities: [] })
  })
}
// usersDuck.js

import createDuck from './remoteObjDuck'

export default createDuck({ namespace: 'my-app', store: 'user', path: '/users' })
// reducers.js

import { combineReducers } from 'redux'
import userDuck from './userDuck'

export default combineReducers({ [userDuck.store]: userDuck.reducer })

Extending Ducks

This example is based on the previous one.

// usersDuck.js

import createDuck from './remoteObjDuck.js'

export default createDuck({ namespace: 'my-app',store: 'user', path: '/users' }).extend({
  types: [ 'RESET' ],
  reducer: (state, action, { types, statuses, initialState }) => {
    switch(action.type) {
      case types.RESET:
        return { ...initialState, obj: { ...initialState.obj, ...action.payload } }
      default:
        return state
  },
  creators: ({ types }) => ({
    reset: (fields) => ({ type: types.RESET, payload: fields }),
  })
})

Creating Reusable Duck Extensions

This example is a refactor of the previous one.

// resetDuckExtension.js

export default {
  types: [ 'RESET' ],
  reducer: (state, action, { types, statuses, initialState }) => {
    switch(action.type) {
      case types.RESET:
        return { ...initialState, obj: { ...initialState.obj, ...action.payload } }
      default:
        return state
  },
  creators: ({ types }) => ({
    reset: (fields) => ({ type: types.RESET, payload: fields }),
  })
}
// userDuck.js

import createDuck from './remoteObjDuck'
import reset from './resetDuckExtension'

export default createDuck({ namespace: 'my-app',store: 'user', path: '/users' }).extend(reset)

Creating Ducks with selectors

Selectors help in providing performance optimisations when used with libraries such as React-Redux, Preact-Redux etc.

// Duck.js

import Duck, { constructLocalized } from 'extensible-duck'

export default new Duck({
  store: 'fruits',
  initialState: {
    items: [
      { name: 'apple', value: 1.2 },
      { name: 'orange', value: 0.95 }
    ]
  },
  reducer: (state, action, duck) => {
    switch(action.type) {
      // do reducer stuff
      default: return state
    }
  },
  selectors: constructLocalized({
    items: state => state.items, // gets the items from state
    subTotal: new Duck.Selector(selectors => state =>
      // Get another derived state reusing previous selector. In this case items selector
      // Can compose multiple such selectors if using library like reselect. Recommended!
      // Note: The order of the selectors definitions matters
      selectors
        .items(state)
        .reduce((computedTotal, item) => computedTotal + item.value, 0)
    )
  })
})
// reducers.js

import { combineReducers } from 'redux'
import Duck from './Duck'

export default combineReducers({ [Duck.store]: Duck.reducer })
// HomeView.js
import React from 'react'
import Duck from './Duck'

@connect(state => ({
  items: Duck.selectors.items(state),
  subTotal: Duck.selectors.subTotal(state)
}))
export default class HomeView extends React.Component {
  render(){
    // make use of sliced state here in props
    ...
  }
}