npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

extensible-duck

v1.6.0

Published

Modular and Extensible Redux Reducer Bundles (ducks-modular-redux)

Downloads

3,021

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 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
    ...
  }
}