Awesome
saga-duck
extensible and composable duck for redux-saga, typescript 3.x supported.
See also ducks-modular-redux extensible-duck
example
Run command:
npm start
Source code are in examples
directory
usage
install
npm i saga-duck -S
Documents
for 2.x please visit Legacy Document-中文
memtion
Ducks should be stateless, so we can use React FSC(functional stateless compoment) and optimize later. You should only access store by duck.selector or duck.selectors.
single duck
import { Duck } from "saga-duck";
import { takeEvery, call, put, select } from "redux-saga/effects";
import { delay } from "redux-saga";
class SingleDuck extends Duck {
get quickTypes() {
return {
...super.quickTypes,
INCREMENT: 1,
INCREMENT_ASYNC: 1
};
}
get reducers() {
const { types } = this;
return {
...super.reducers,
count: (state = 0, action) => {
switch (action.type) {
case types.INCREMENT:
return state + 1;
default:
return state;
}
}
};
}
*saga() {
yield* super.saga();
const { types, selector } = this;
yield takeEvery(types.INCREMENT_ASYNC, function*() {
yield call(delay, 1000);
// select state of this duck
const { count } = selector(yield select());
yield put({type: types.INCREMENT});
});
}
}
extend duck
class ExtendedDuck extends SingleDuck {
get quickTypes(){
return {
...super.quickTypes,
MORE: 1
}
}
get reducers(){
return {
...super.reducers,
more: (state, action) => 1
}
}
get rawSelectors(){
return {
...super.rawSelectors,
more(state){
return state.more
}
}
}
get creators(){
const { types } = this
return {
...super.creators,
more(){
return {
type: types.MORE
}
}
}
}
*saga(){
yield* super.saga()
const { types, selector, selectors, creators } = this
yield take([types.INCREMENT, types.MORE])
const { count, more } = selector(yield select())
const _more = selectors.more(yield select())
yield put(creators.more())
}
}
compose ducks
import { ComposableDuck } from "saga-duck";
class ComposedDuck extends ComposableDuck {
get quickTypes() {
return {
...super.quickTypes,
PARENT: 1
};
}
get quickDucks() {
return {
...super.quickDucks,
duck1: SingleDuck,
duck2: ExtendedDuck,
duck3: ExtendedDuck
};
}
*saga() {
yield* super.saga();
const {
types,
selector,
ducks: { duck1, duck2, duck3 }
} = this;
yield takeEvery(types.PARENT, function*() {
yield put({ type: duck1.types.INCREMENT });
yield put(duck2.creators.more());
yield put(duck3.creators.more());
});
// { parent, duck1: {count}, duck2: {count, more}, duck3: {count, more} }
const state = selector(yield select());
}
}
Run and connect to React (Legacy style)
import { DuckRuntime } from "saga-duck";
import Root from "./Root";
import Duck from "./RootDuck";
const duckRuntime = new DuckRuntime(new Duck({...}));
const ConnectedComponent = duckRuntime.connectRoot()(Root);
ReactDOM.render(
<Provider store={duckRuntime.store}>
<ConnectedComponent />
</Provider>,
document.getElementById("root")
);
Root.ts (Duck Component)
import * as React from 'react'
import Counter from "./Counter";
import { DuckCmpProps } from 'saga-duck';
import Duck from './RootDuck'
export default function Root({ duck, store, dispatch }: DuckCmpProps<Duck>) {
const { selectors, creators, ducks: { counter1 } } = duck;
return (
<div>
counter1:
<Counter duck={counter1} store={store} dispatch={dispatch} />
myself: total increment times: {selectors.total(store)} <br/>
<button onClick={()=>dispatch(creators.increment())}>
Increment all
</button>
</div>
);
}
Run and connect to React (hook style)
import * as React from 'react'
import Counter from "./Counter";
import Duck from './RootDuck'
import { useDuck } from 'saga-duck'
export default function Root() {
const { duck, store, dispatch } = useDuck(Duck)
const { selectors, creators, ducks: { counter1 } } = duck;
return (
<div>
counter1:
<Counter duck={counter1} store={store} dispatch={dispatch} />
myself: total increment times: {selectors.total(store)} <br/>
<button onClick={()=>dispatch(creators.increment())}>
Increment all
</button>
</div>
);
}
Helpers
useDuck
Connect duck to react in hook style.
function MyCmp(){
const { duck, store, dispatch } = useDuck(MyDuck)
const { selector, creators } = duck;
return <>{selector(store).xxx}</>
}
purify
make React DuckComponent pure, only rerender when props and duck state changed.
import { purify } from 'saga-duck'
export default purify(function DuckComponent({ duck, store, dispatch }){ ... })
memorize
stabilize objects/functions reference, prevent React props unnecessary change.
With React 16, you can use useMemo
or useCallback
hooks instead
import { memorize } from 'saga-duck'
const getHandler = memorize((duck, dispatch) => ()=>dispatch(duck.creators.bar()) )
function Container(props){
const handler = gethandler(props)
return <Foo handler={handler} />
}
reduceFromPayload / createToPayload
Create simple reducer / actionCreator
import { reduceFromPayload, createToPayload } from 'saga-duck'
let reducer = reduceFromPayload(types.SET_ID, 0)
// equal to
reducer = (state=0, action)=>{
switch(action.type){
case types.SET_ID:
return action.payload
default:
return state
}
}
let creator = createToPayload<number>(types.SET_ID)
// equal to
creator = (id: number)=>({ type: types.SET_ID, payload: id })
Typescript support
See Duck example and ComposableDuck example, please use typescript 3.0+ for saga-duck 3.x, and typescript 2.6.1 for saga-duck 2.x.