extensible-duck is an implementation of the Ducks proposal. With this library you can create reusable and extensible ducks.
// 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 })
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
{ root: state => state}
duck => ({ root: state => state })
(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.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).
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 }) }) })
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 }) })
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; } ) ) }) })
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" ] }
export default new Duck({ // ... consts: { statuses: ['NEW'], // myDuck.statuses = { NEW: "NEW" } fooBar: [ 'FOO', // myDuck.fooBar.FOO = "FOO" 'BAR' // myDuck.fooBar.BAR = "BAR" ] } }
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 })
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 ... } }
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4