extensible-duck is an implementation of the Ducks proposal. With this library you can create reusable and extensible ducks.
- Basic Usage
- Creating Reusable Ducks
- Extending Ducks
- Creating Reusable Duck Extensions
- Creating Ducks with selectors
// 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 or function(duck) |
{ root: state => state} or duck => ({ root: state => state }) |
- duck.store
- duck.storePath
- duck.reducer
- duck.creators
- duck.sagas
- duck.takes
- duck.selectors
- duck.types
- for each const, duck.<const>
- constructLocalized(selectors): maps selectors syntax from
(globalStore) => selectorBody
into(localStore, globalStore) => selectorBody
.localStore
is derived fromglobalStore
on every selector execution usingduck.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:
- It receives the duck itself as the third argument
- It doesn't define the initial state (see Defining the Initial State)
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)
])
})
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 }),
})
})
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, { 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
...
}
}