ducks
🦆🦆🦆Ducks is a Reducer Bundles Manager that Implementing the Redux Ducks Modular Proposal with Great Convenience.
Image Credit: Alamy
Ducks offers a method of handling redux module packaging, installing, and running with your Redux store, with middleware support.
Java has jars and beans. Ruby has gems. I suggest we call these reducer bundles "ducks", as in the last syllable of "redux".
— Erik Rasmussen, 2015 (link)
Goal
The goal of Ducks is to:
- Organizing your code for the long term.
- Maximum your convenience when using Redux Ducks.
- Type-safe with strong typing with TypeScript Generic Templates.
Features
- Implemented the specification from Ducks Modular Proposal, Erik Rasmussen, 2015
- Easy connecting ducks to store by adding one enhancer to redux. (that's all you need to do!)
- Fully typing with all APIs by TypeScript
- Currying
operators
and selectors
by binding the Store
to them for maximum convenience.
Todo-list:
Motivation
I'm building my redux ducks module for Wechaty Redux project and ...
To be written.
At last, I decide to write my own manager for ducks, which will implement the following two specifications, with my own Ducksify Extension:
- The Ducks Modular Proposal
- The Re-Ducks Extension: Duck Folders
- The Ducksify Extension: Currying
selectors
and operators
1 The Ducks Modular Proposal
The specification has rules that a module:
- MUST
export default
a function called reducer()
- MUST
export
its action creators as functions - MUST have action types in the form
npm-module-or-app/reducer/ACTION_TYPE
- MAY export its action types as
UPPER_SNAKE_CASE
, if an external reducer needs to listen for them, or if it is a published reusable library
Here's the full version of Ducks proposal: Redux Reducer Bundles, A proposal for bundling reducers, action types and actions when using Redux, Erik Rasmussen, 2015
2 The Re-Ducks Extension: Duck Folders
Re-Ducks
is an extension to the original proposal for the ducks redux modular architecture.
By defining a ducks with duck folders instead of a duck file, it defines the duck folder would like:
duck/
├── actions.js
├── index.js
├── operations.js
├── reducers.js
├── selectors.js
├── tests.js
├── types.js
├── utils.js
NOTE: Each concept from your app will have a similar folder.
General rules for a duck folder
A duck folder:
- MUST contain the entire logic for handling only ONE concept in your app, ex: product, cart, session, etc.
- MUST have an
index.js
file that exports according to the original duck rules. - MUST keep code with similar purpose in the same file, ex: reducers, selectors, actions, etc.
- MUST contain the tests related to the duck.
Here's the full version of Re-ducks proposal: Building on the duck legacy, An attempt to extend the original proposal for redux modular architecture, Alex Moldovan, 2016 and blog
3 Ducksify Extension: Currying & Ducksify Interface
In order to build a fully modularized Ducks, we define the Ducksify extension with the following rules:
-
MUST export its module interface as the following Duck
interface:
export interface Duck {
default: Reducer,
actions : ActionCreatorsMapObject,
operations?: OperationsMapObject,
selectors? : SelectorsMapObject,
types? : TypesMapObject,
middlewares?: MiddlewaresMapObject,
epics?: EpicsMapObject,
setDucks?: (ducks: Ducks<any>) => void
}
-
MUST support Currying the first argument for selectors.*
with a State
object
-
MUST support Currying the first argument for operations.*
with a Dispatch
function
-
MAY export its middlewares functions called *Middleware()
-
MAY export its saga functions called *Saga()
-
MAY export its epic functions called *Epic()
-
MAY use typesafe-actions to creating reducers, actions, and middlewares.
-
If we has sagas
, epics
, or middlewares
, the duck folder would like:
duck/
├── epics.js
├── sagas.js
├── middlewares.js
Requirements
Node.js v16+, or Browser with ES2020 Support
Install
npm install ducks
Usage
1 Create Redux Reducer Bundle as a Duck
For example, let's create a Duck module file named counter.ts
:
export const types = { TAP: 'ducks/examples/counter/TAP' }
export const actions = { tap: () => ({ type: TAP }) }
export const operations = { tap: dispatch => dispatch(actions.tap()) }
export const selectors = { getTotal: state => () => state.total }
const initialState = { total: 0 }
export default function reducer (state = initialState, action) {
if (action.type === types.TAP) {
return ({
...state,
total: (state.total || 0) + 1,
})
}
return state
}
2 Manage the Bundles with Ducks
Manager
import { Ducks } from 'ducks'
import * as counterDuck from './counter.js'
const ducks = new Ducks({ counter: counterDuck })
const counter = ducks.ducksify(counterDuck)
3 Configure Redux Store
import { createStore } from 'redux'
const store = createStore(
state => state,
ducks.enhancer(),
)
You are all set!
4 Using Ducks
The Vanilla Style and Ducksify Style is doing exactly the same thing.
The Vanilla Style
store.dispatch(counterApi.actions.tap())
console.info('getTotal:', counterApi.selectors.getTotal(store.getState().counter)))
The Ducksify Style
counter.operations.tap()
console.info('getTotal:', counter.selectors.getTotal()))
It turns out that the Ducksify Style is more clear and easy to use by currying them with the store as their first argument.
That's it!
Examples
Let's get to know more about Ducks by quack!
The following is the full example which demonstrate how to use Ducks.
It shows that:
- How to import duck modules with easy and clean way.
- Ducks supports
redux-observable
and redux-saga
out-of-the-box with zero configuration. - How to stick with the best practices to write a redux reducer bundle by following the ducks modular proposal.
Talk is cheap, show me the code
The following example code can be found at examples/quack.ts, you can try it by running the following commands:
git clone git@github.com:huan/ducks.git
cd ducks
npm install
npm start
import { createStore } from 'redux'
import { Duck, Ducks } from 'ducks'
import * as counterDuck from './counter.js'
import * as dingDongDuck from './ding-dong.js'
import * as pingPongDuck from './ping-pong.js'
import * as switcherDuck from './switcher.js'
const ducks = new Ducks({
counter : counterDuck,
switcher : switcherDuck,
dingDong : dingDongDuck,
pingPong : pingPongDuck,
})
const {
counter,
dingDong,
pingPong,
switcher,
} = ducks.ducksify()
const store = createStore(
state => state,
ducks.enhancer(),
)
assert.strictEqual(counter.selectors.getCounter(), 0)
counter.operations.tap()
assert.strictEqual(counter.selectors.getCounter(), 1)
assert.strictEqual(switcher.selectors.getStatus(), false)
switcher.operations.toggle()
assert.strictEqual(switcher.selectors.getStatus(), true)
assert.strictEqual(dingDong.selectors.getDong(), 0)
dingDong.operations.ding()
assert.strictEqual(dingDong.selectors.getDong(), 1)
assert.strictEqual(pingPong.selectors.getPong(), 0)
pingPong.operations.ping()
assert.strictEqual(pingPong.selectors.getPong(), 1)
console.info('store state:', store.getState())
I hope you will like this clean and beautiful Ducksify
way with using Ducks!
Api References
Ducks is very easy to use, because one of the goals of designing it is to maximum the convenience.
We use Ducks
to manage Redux Reducer Bundle
s with the Duck
interface that following the ducks modular proposal.
For validating your Duck
form the redux module (a.k.a reducer bundle), we have a validating helper function validateDuck
that accepts a Duck
to make sure it's valid (it will throws an Error when it's not).
1 Duck
The Duck
is a interface which is defined from the ducks modular proposal, extended from both Re-Ducks and Ducksify.
Example:
Duck
counter example from our examples
import * as actions from './actions.js'
import * as operations from './operations.js'
import * as selectors from './selectors.js'
import * as types from './types.js'
import reducer from './reducers.js'
export {
actions,
operations,
selectors,
types,
}
export default reducer
2 Ducks
The Ducks
class is the manager for Duck
s and connecting them to the Redux Store by providing a enhancer()
to Redux createStore()
.
import { Ducks } from 'ducks'
import * as counterApi from './counter.js'
const ducks = new Ducks({
counter: counterApi,
})
const store = createStore(
state => state,
ducks.enhancer(),
)
There is one important thing that we need to figure out is that when we are passing the DucksMapObject
to initialize the Ducks
({ counter: counterDuck }
in the above case), the key name of this Api will become the mount point(name space) for its state.
Choose your key name wisely because it will inflect the state structure and the typing for your store.
There's project named Ducks++: Redux Reducer Bundles, Djamel Hassaine, 2017 to solve the mount point (namespace) problem, however, we are just use the keys in the DucksMapObject
to archive the same goal.
2.1 Ducks#enhancer()
Returns a StoreEnhancer
for using with the Redux store creator, which is the most important and the only one who are in charge of initializing everything for the Ducks.
const store = createStore(
state => state,
ducks.enhancer(),
)
If you have other enhancers need to be used with the Ducks, for example, the applyMiddleware()
enhancer from the Redux, you can use compose()
from Redux to archive that:
import { applyMiddleware, compose, createStore } from 'redux'
import { Ducks } from 'ducks'
const store = createStore(
state => state,
compose(
ducks.enhancer(),
applyMiddleware(
),
)
)
NOTE: our enhancer()
should be put to the most left in the compose()
argument list, because it would be better to make it to be the most outside one to be called.
2.2 Ducks#configureStore()
If you only use Redux with Ducks without any other reducers, then you can use configureStore()
shortcut from the Ducks to get the configured store.
const store = ducks.configureStore(preloadedStates)
The above code will be equals to the following naive Redux createStore()
codes because the configureStore()
is just a shortcut of that for our convenience.
const store = createStore(
state => state,
preloadedStates,
ducks.enhancer(),
)
2.3 Ducks#ducksify()
ducksify()
will encapsulate the Api
into the Bundle
class so that we will have a more convenience way to use it.
- Return all Bundles:
const { counter } = ducks.ducksify()
- Return the Bundle for namespace: `const counter = ducks.ducksify('counter')
- Return the Bundle for api: `const counter = ducks.ducksify(counterApi)
For example:
import * as counterDuck from './counter.js'
const ducks = new Ducks({ counter: counterDuck })
const store = ducks.configureStore()
const { counter } = ducks.ducksify()
const counterByName = ducks.ducksify('counter')
assert(counterByName === counter)
const counterByApi = ducks.ducksify(counterDuck)
assert(counterByApi === counter)
Comparing the Duck with the Bundle (ducksified Duck), we will get the following differences: (counterBundle
is the ducksified counterDuck
)
For selectors
:
- counterDuck.selectors.getTotal(store.getState().counter)()
+ counterBundle.selectors.getTotal()
For operations
:
- counterDuck.operations.tap(store.dispatch)()
+ counterBundle.operations.tap()
As you see, the above differences showed that the ducksified api will give you great convenience by currying the Store
inside itself.
4 validateDuck()
To make sure your Ducks Api is following the specification of the ducks modular proposal, we provide a validating function to check it.
import { validateDuck } from 'ducks'
import * as counterDuck from './counter.js'
validateDuck(counterDuck)
Resources
Modular
Middlewares
Relate Libraries
- Microsoft Redux Dynamic Modules: Modularize Redux by dynamically loading reducers and middlewares.
- ioof-holdings/redux-dynostore - These libraries provide tools for building dynamic Redux stores.
- reSolve - A Redux-Inspired Backend
- redux-dynamic-middlewares - Allow add or remove redux middlewares dynamically
Other Links
Future Thoughts
Redux Ducks Api compares with CQRS, Event Sourcing, and DDD:
Ducks | CQRS | Event Sourcing | DDD |
---|
actions | Domain Aggregates with Command handlers | | |
- creator | Command | | |
- payload | Event | Event | |
selectors | Query | | |
operations | Command + Event | | |
middlewares | Aggregate? | Saga ? | |
types | ?? | | |
reducers | Reducers to calculate Aggregate state | | |
reSolve is a Node.js library for Redux & CQRS
Domain Driven Design (DDD)
Domain aggregate is a business model unit. Business logic is mostly in command handlers for the aggregate.
Event Sourcing (ES)
Don't store system state, store events that brought system to this state.
Command Query Responsibility Segregation (CQRS)
CQRS system is divided in two "sides":
- Write Side accepts commands and generate events that stored in the Event Store.
- Read Side applies events to Read Models, and process queries.
History
master v1.0 (Oct 29, 2021)
Release v1.0 of Redux Ducks
v0.11 (Sep 2021)
- Disable
saga
support temporary due to (#4) - ES Modules support
v0.10 (Jun 6, 2020)
Add setDucks()
to Duck
API interface,
so that all the Duck
can get the Ducks
instance
(if needed, by providing a setDucks()
method from the API),
which helps the Duck
s talk to each others.
v0.8 (Jun 5, 2020)
Renaming for better names with a better straightforward intuition.
- Rename
interface Api
to interface Duck
- Rename
class Duck
to class Bundle
- Rename
function validateDucksApi
to function validateDuck
v0.6 (Jun 1, 2020)
Refactoring the Ducks
with better Api interface.
- Added
ducksify()
method for get Duck
instance by namespace
or api
.
v0.4 (May 30, 2020)
Fix the TypeScript Generic Template typing problems:
- Protect String Literal Types in Action Types #1
- Property 'payload' is missing in type 'AnyAction' #2
v0.2 (May, 1 2020)
- Published the very first version of Ducks Modular Proposal to Ducks!
Thanks
@gobwas is the gentleman who owned this ducks NPM module name, and he's so kind for letting me use this great NPM module name ducks
for my project. Appreciate it!
Badge
Powered by Ducks
[![Powered by Ducks](https://img.shields.io/badge/Powered%20by-Ducks-yellowgreen)](https://github.com/huan/ducks#3-ducksify-extension-currying--ducksify-interface)
Ducksify
[![Ducksify Extension](https://img.shields.io/badge/Redux-Ducksify%202020-yellowgreen)](https://github.com/huan/ducks#3-ducksify-extension-currying--ducksify-interface)
Author
Huan LI (李卓桓), Microsoft Regional Director, <zixia@zixia.net>
Copyright & License
- Code & Docs © 2020 Huan LI (李卓桓) <zixia@zixia.net>
- Code released under the Apache-2.0 License
- Docs released under Creative Commons