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 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({ widgets: 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 | String | 'widgets' |
| 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 }) |
| selectors | state selectors | Object of functions | { root: state => state} |
- duck.reducer
- duck.creators
- duck.selectors
- duck.types
- for each const, duck.<const>
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({ user: 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 }),
})
})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)Selectors help in providing performance optimisations when used with libraries such as React-Redux, Preact-Redux etc.
// Duck.js
import Duck from 'extensible-duck'
export new Duck({
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: {
items: state => state.items, // gets the items from state
subTotal: 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)
}
})// 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
...
}
}