Giter Site home page Giter Site logo

huan / ducks Goto Github PK

View Code? Open in Web Editor NEW
0.0 4.0 0.0 1.37 MB

Reducer Bundles Manager for Redux, Implemented the Ducks Modular Proposal

Home Page: https://paka.dev/npm/ducks

License: Apache License 2.0

JavaScript 39.24% Shell 2.68% TypeScript 58.08%
redux ducks redux-ecosystem-links modular-ducks redux-reducer-bundle

ducks's Introduction

ducks

NPM NPM Version NPM

πŸ¦†πŸ¦†πŸ¦†Ducks is a Reducer Bundles Manager that Implementing the Redux Ducks Modular Proposal with Great Convenience.

Ducks

Image Credit: Alamy

Ducks Modular Proposal Re-Ducks Extended Ducksify Extension

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:

  1. Organizing your code for the long term.
  2. Maximum your convenience when using Redux Ducks.
  3. Type-safe with strong typing with TypeScript Generic Templates.

Features

  1. Implemented the specification from Ducks Modular Proposal, Erik Rasmussen, 2015
  2. Easy connecting ducks to store by adding one enhancer to redux. (that's all you need to do!)
  3. Fully typing with all APIs by TypeScript
  4. Currying operators and selectors by binding the Store to them for maximum convenience.

Todo-list:

  • Ducks middleware support
  • Provides a Ducks Management interface for adding/deleting a duck module dynamically

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:

  1. The Ducks Modular Proposal
  2. The Re-Ducks Extension: Duck Folders
  3. The Ducksify Extension: Currying selectors and operators

1 The Ducks Modular Proposal

Ducks Modular Proposal

The specification has rules that a module:

  1. MUST export default a function called reducer()
  2. MUST export its action creators as functions
  3. MUST have action types in the form npm-module-or-app/reducer/ACTION_TYPE
  4. 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 Extension

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:

  1. MUST contain the entire logic for handling only ONE concept in your app, ex: product, cart, session, etc.
  2. MUST have an index.js file that exports according to the original duck rules.
  3. MUST keep code with similar purpose in the same file, ex: reducers, selectors, actions, etc.
  4. 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

Ducksify Extension

In order to build a fully modularized Ducks, we define the Ducksify extension with the following rules:

  1. MUST export its module interface as the following Duck interface:

    export interface Duck {
      /**
       * Ducks Modular Proposal (https://github.com/erikras/ducks-modular-redux)
      */
      default: Reducer,
    
      actions    : ActionCreatorsMapObject,
      operations?: OperationsMapObject,
      selectors? : SelectorsMapObject,
      types?     : TypesMapObject,
    
      /**
       * Ducksify Extension (https://github.com/huan/ducks#3-ducksify-extension-currying--ducksify-interface)
      */
      middlewares?: MiddlewaresMapObject,
      epics?: EpicsMapObject,
      // Disabled temporary. See: https://github.com/huan/ducks/issues/4
      // sagas?: SagasMapObject,
    
      setDucks?: (ducks: Ducks<any>) => void
    }
  2. MUST support Currying the first argument for selectors.* with a State object

  3. MUST support Currying the first argument for operations.* with a Dispatch function

  4. MAY export its middlewares functions called *Middleware()

  5. MAY export its saga functions called *Saga()

  6. MAY export its epic functions called *Epic()

  7. MAY use typesafe-actions to creating reducers, actions, and middlewares.

  8. 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,     // Your other reducers
  ducks.enhancer(),   // Add ducks to your store (that's all you need to do!)
)

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)))
// Output: getTotal: 1

The Ducksify Style

counter.operations.tap()
console.info('getTotal:', counter.selectors.getTotal()))
// Output: getTotal: 2

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:

  1. How to import duck modules with easy and clean way.
  2. Ducks supports redux-observable and redux-saga out-of-the-box with zero configuration.
  3. 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 [email protected]:huan/ducks.git
cd ducks

npm install
npm start
import { createStore } from 'redux'
import { Duck, Ducks } from 'ducks'

import * as counterDuck  from './counter.js'    // Vanilla Duck: +1
import * as dingDongDuck from './ding-dong.js'  // Observable Middleware
import * as pingPongDuck from './ping-pong.js'  // Saga Middleware
import * as switcherDuck from './switcher.js'   // Type Safe Actions: ON/OFF

const ducks = new Ducks({
  counter  : counterDuck,
  switcher : switcherDuck,
  dingDong : dingDongDuck,
  pingPong : pingPongDuck,
})

const {
  counter,
  dingDong,
  pingPong,
  switcher,
} = ducks.ducksify()

const store = createStore(
  state => state,     // Here's our normal Redux Reducer
  ducks.enhancer(),   // We use Ducks by adding this enhancer to our store, and that's it!
)

/**
 * Vanilla: Counter
 */
assert.strictEqual(counter.selectors.getCounter(), 0)
counter.operations.tap()
assert.strictEqual(counter.selectors.getCounter(), 1)

/**
 * TypeSafe Actions: Switchers
 */
assert.strictEqual(switcher.selectors.getStatus(), false)
switcher.operations.toggle()
assert.strictEqual(switcher.selectors.getStatus(), true)

/**
 * Epic Middleware: DingDong
 */
assert.strictEqual(dingDong.selectors.getDong(), 0)
dingDong.operations.ding()
assert.strictEqual(dingDong.selectors.getDong(), 1)

/**
 * Saga Middleware: PingPong
 */
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 Bundles 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 Ducks 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(),
)
// Duck will be ready to use after the store has been created.

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.

// This is exactly what `ducks.configureStore()` does:
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.

  1. Return all Bundles: const { counter } = ducks.ducksify()
  2. Return the Bundle for namespace: `const counter = ducks.ducksify('counter')
  3. 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()

// 1. Return all Bundles
const { counter } = ducks.ducksify()

// 2. Return the Bundle for namespace: `counter`
const counterByName = ducks.ducksify('counter')
assert(counterByName === counter)

// 3. Return the Bundle for api: `counterDuck`
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) // will throw if the counterApi is invalid.

Resources

Modular

Middlewares

Relate Libraries

  1. Microsoft Redux Dynamic Modules: Modularize Redux by dynamically loading reducers and middlewares.
  2. ioof-holdings/redux-dynostore - These libraries provide tools for building dynamic Redux stores.
  3. reSolve - A Redux-Inspired Backend
  4. 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":

  1. Write Side accepts commands and generate events that stored in the Event Store.
  2. 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)

  1. Disable saga support temporary due to (#4)
  2. 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 Ducks talk to each others.

v0.8 (Jun 5, 2020)

Renaming for better names with a better straightforward intuition.

  1. Rename interface Api to interface Duck
  2. Rename class Duck to class Bundle
  3. Rename function validateDucksApi to function validateDuck

v0.6 (Jun 1, 2020)

Refactoring the Ducks with better Api interface.

  1. Added ducksify() method for get Duck instance by namespace or api.

v0.4 (May 30, 2020)

Fix the TypeScript Generic Template typing problems:

  1. Protect String Literal Types in Action Types #1
  2. Property 'payload' is missing in type 'AnyAction' #2

v0.2 (May, 1 2020)

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

[![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

[![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, <[email protected]>

Profile of Huan LI (ζŽε“ζ‘“) on StackOverflow

Copyright & License

  • Code & Docs Β© 2020 Huan LI (ζŽε“ζ‘“) <[email protected]>
  • Code released under the Apache-2.0 License
  • Docs released under Creative Commons

ducks's People

Contributors

huan avatar

Watchers

 avatar  avatar  avatar  avatar

ducks's Issues

Promote the Ducks library

Property 'payload' is missing in type 'AnyAction'

Minimum reproducible code snip:

import { Reducer, AnyAction } from 'redux'

type MyAction = {
  payload: number,
  type: 'TAP',
}

const f: Reducer<any, MyAction> = (s: any, ..._: any[]) => s
const r: Reducer<any, AnyAction> = f
// r is NOT ok.
void r

The error message of r is:

Type 'Reducer<any, MyAction>' is not assignable to type 'Reducer<any, AnyAction>'.
  Types of parameters 'action' and 'action' are incompatible.
    Type 'AnyAction' is not assignable to type 'MyAction'.ts(2322)

However,

const a1: MyAction = {
  payload: 42,
  type: 'TAP',
}
const a2: AnyAction = a1
// a2 is OK
void a2

To be fixed.

Related Issues

createMockStore is not a function

When running with ES Module mode, we have to:

import * as MOCK_STORE 'redux-mock-store'
const createMockStore = (MOCK_STORE.default as any).default as typeof MOCK_STORE.default

The above code will violate the Typing system (must use as any), but it works for the final code.

To be investigated.

Version

  • "redux-mock-store": "^1.5.4"
  • "@types/redux-mock-store": "^1.0.3"

Link to

Protect String Literal Types in Action Types

When we define the redux action types in ducks, we will store them in a types.ts file:

const TEST = 'module/TEST'

Use export const TEST

And use import * as types from './types' in other TS module to import all the types.

In this way, the TEST can keep the string literal type as module/TEST, which is necessary for future usage.

Do NOT use export default { TEST }

In this way, the typeof TEST will become string, which will lose its string literal type.

If it lists its string literal type, then it seems more likely will cause the problem in future usage.

References

Using string constants as action type property:

please make sure to use simple string literal assignment with const. This limitation is coming from the type-system, because all the dynamic string operations (e.g. string concatenation, template strings and also object used as a map) will widen the literal type to its super-type, string. As a result this will break contextual typing for action object in reducer cases.

import 'redux-saga/effects': CustomError: ERR_UNSUPPORTED_DIR_IMPORT

The Problem

When running under ES Modules mode:

$ node --no-warnings --loader=ts-node/esm ./src/ducks/ducks.spec.ts 
/Users/huan/git/huan/ducks/node_modules/ts-node/dist-raw/node-esm-resolve-implementation.js:379
    const err = new ERR_UNSUPPORTED_DIR_IMPORT(path, fileURLToPath(base));
                ^
CustomError: ERR_UNSUPPORTED_DIR_IMPORT /Users/huan/git/huan/ducks/node_modules/redux-saga/effects /Users/huan/git/huan/ducks/examples/ping-pong/sagas.ts

import {
takeEvery,
put,
} from 'redux-saga/effects'

The Workaround

To disable saga temporarily and will investigate in the future.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.