1.6.0 • Published 5 years ago

extensible-duck v1.6.0

Weekly downloads
1,051
License
MIT
Repository
github
Last release
5 years ago

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 npm.io

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 functionsorfunction(duck){ root: state => state}orduck => ({ 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
    ...
  }
}
1.6.0

5 years ago

1.5.0

5 years ago

1.4.0

6 years ago

1.3.1

7 years ago

1.3.0

7 years ago

1.2.1

7 years ago

1.2.0

7 years ago

1.1.1

7 years ago

1.1.0

7 years ago

1.0.2

7 years ago

1.0.1

7 years ago

1.0.0

7 years ago