Extensible Duck

Modular and Extensible Redux Reducer Bundles (ducks-modular-redux)
Alternatives To Extensible Duck
Project NameStarsDownloadsRepos Using ThisPackages Using ThisMost Recent CommitTotal ReleasesLatest ReleaseOpen IssuesLicenseLanguage
Immer25,98287,9804,0523 days ago170May 09, 202341mitJavaScript
Create the next immutable state by mutating the current one
Redux Toolkit9,895252,63521 hours ago75May 30, 2023255mitTypeScript
The official, opinionated, batteries-included toolset for efficient Redux development
Ducks Modular Redux9,156
2 years ago36JavaScript
A proposal for bundling reducers, action types and actions when using Redux
Rematch8,4012101568 days ago76November 09, 202123mitTypeScript
The Redux Framework
Reswift7,446
155a month ago9January 15, 201847mitSwift
Unidirectional Data Flow in Swift - Inspired by Redux
Redux Ecosystem Links5,201
4 months ago31
A categorized list of Redux-related addons, libraries, and utilities
Connected React Router4,7312,0667107 months ago51July 11, 2022175mitJavaScript
A Redux binding for React Router v4
Store3,98226 years ago8June 07, 201710mitTypeScript
RxJS powered state management for Angular applications, inspired by Redux
Redux Orm2,96582379 months ago70August 11, 2021115mitJavaScript
NOT MAINTAINED – A small, simple and immutable ORM to manage relational data in your Redux store.
Redux Undo2,8556372093 months ago41July 17, 202335mitJavaScript
:recycle: higher order reducer to add undo/redo functionality to redux state containers
Alternatives To Extensible Duck
Select To Compare


Alternative Project Comparisons
Readme

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

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

Name Description Type Example
namespace Used as a prefix for the types String 'my-app'
store Used as a prefix for the types and as a redux state key String 'widgets'
storePath Object 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 state String 'foo.bar'
types List of action types Array [ 'CREATE', 'UPDATE' ]
consts Constants you may need to declare Object of Arrays { statuses: [ 'LOADING', 'LOADED' ] }
initialState State passed to the reducer when the state is undefined Anything {}
reducer Action reducer function(state, action, duck) (state, action, duck) => { return state }
creators Action creators function(duck) duck => ({ type: types.CREATE })
sagas Action sagas function(duck) duck => ({ fetchData: function* { yield ... }
takes Action takes function(duck) duck => ([ takeEvery(types.FETCH, sagas.fetchData) ])
selectors state selectors Object of functions
or
function(duck)
{ root: state => state}
or
duck => ({ root: state => state })

Duck Accessors

  • duck.store
  • duck.storePath
  • duck.reducer
  • duck.creators
  • duck.sagas
  • duck.takes
  • duck.selectors
  • duck.types
  • for each const, duck.<const>

Helper functions

  • constructLocalized(selectors): maps selectors syntax from (globalStore) => selectorBody into (localStore, globalStore) => selectorBody. localStore is derived from globalStore on every selector execution using duck.storage key. Use to simplify selectors syntax when used in tandem with reduxes' combineReducers to bind the duck to a dedicated state part (example). If defined will use the duck.storePath value to determine the localized state in deeply nested redux state trees.

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
    ...
  }
}
Popular Reducer Projects
Popular Redux Projects
Popular User Interface Categories

Get A Weekly Email With Trending Projects For These Categories
No Spam. Unsubscribe easily at any time.
Javascript
Redux
Types
Selector
Reducer
Sagas