dx-saga
v0.0.1-beta.6
Published
Warning: This package is in beta and subject to change frequently check back often for the latest.
Downloads
9
Readme
dx-saga
Warning: This package is in beta and subject to change frequently check back often for the latest.
dx-saga is a JavaScript library that allows redux-sagas to run on differences in state as opposed to actions.
selectorChannel
- run sagas when a subset of state changes as opposed to when actions are dispatched
- prevent extraneous side-effects by only running sagas when the subset of state used as inputs changes
- simplify sagas that take multiple actions as inputs by watching the state instead
- nextAction = F(select(state), saga) where select(state) ⊂ state when select(State) != select(nextState)
useSaga
- Start and stop sagas when components mount and unmount
- ensure effects, like takeLatest, have their own state per UI component
- provide component identity and other props using the
ownProps
option - optionally provide a separate
context
from the saga middleware saga
- serialize execution of code blocks using
monitor.lock
selectorChannel
usage
const getSearchChanges = (state: RootState): SearchChanges => {
const { text, caseSensitive } = state.search;
return { text, caseSensitive };
};
function* handleSearchChanges(searchChanges: SearchChanges) {
// ...
}
function* watchSearchSagas() {
/* HANDLE CHANGES TO STATE AS OPPOSED TO ACTIONS. ACCEPTS ANY SELECTOR */
const searchChanges = selectorChannel(getSearchChanges);
/* USE WHERE PATTERNS ARE USED */
yield* takeLatest(searchChanges, handleSearchChanges);
}
Installation
# NPM
npm install dx-saga
or
# YARN
yarn add dx-saga
Selector Channels
Motivation for selector channels
https://codesandbox.io/s/take-latest-action-pattern-tux36?file=/src/index.tsx
const getSearchChanges = (state: RootState): SearchChanges => {
const { text, caseSensitive } = state.search;
return { text, caseSensitive };
};
function* handleSearchChanges() {
debug("delay");
yield* delay(500);
const searchChanges = yield* select(getSearchChanges);
debug(`handleSearchChanges ${JSON.stringify(searchChanges)}`);
}
function* watchSearchSagas() {
yield* takeLatest(
[
searchSlice.actions.onChangeCaseSensitive.type,
searchSlice.actions.onChangeText.type,
],
handleSearchChanges
);
}
sagaMiddleware.run(watchSearchSagas);
/* DISPATCH ACTION1 */
const action1 = searchSlice.actions.onChangeText("foo");
store.dispatch(action1);
/* IMMEDIATELY DISPATCH ACTION2. IT WILL CANCEL ACTION1'S SIDE EFFECTS
SEE THE CONSOLE */
const action2 = searchSlice.actions.onChangeCaseSensitive(true);
store.dispatch(action2);
In the above example, takeLatest
watches for action types to trigger sagas. This works well when it's one action. When it's more than one, the saga will not have all the state it needs and it may be triggered unnecessarily when state in the action payload either contains extra data or doesn't result in a change to state. selectorChannel
avoids each of a these cases, resulting in simpler code while leveraging the saga API.
Creating a selectorChannel
We would like to replace takeLatest(pattern, saga)
, which triggers when events occur, with takeLatest(channel, saga)
that triggers when changes occur in the subset of state returned by a selector.
dx-saga
provides a function makeSelectorChannelFactory
that produces a function selectorChannel
to create selector channels. Its accepts any selector and will emit
when subset of state returned by the selector changes. Each of these emissions can be used by existing saga API to takeEvery
, takeLatests
, etc.
Let's define a selectorChannel named searchChanges
to replace the action-pattern version above:
https://codesandbox.io/s/selector-channel-qepep?file=/src/index.tsx:1264-1412
import { makeSelectorChannelFactory } from "dx-saga";
//...
const selectorChannel = makeSelectorChannelFactory(store);
const getSearchChanges = (state: RootState): SearchChanges => {
const { text, caseSensitive } = state.search;
return { text, caseSensitive };
};
function* handleSearchChanges(searchChanges: SearchChanges) {
debug("delay");
yield* delay(500);
debug(`handleSearchChanges ${JSON.stringify(searchChanges)}`);
}
function* watchSearchSagas() {
/* HANDLE CHANGES TO STATE AS OPPOSED TO ACTIONS. ACCEPTS ANY SELECTOR */
const searchChanges = selectorChannel(getSearchChanges);
/* USE WHERE PATTERNS ARE USED */
yield* takeLatest(searchChanges, handleSearchChanges);
}
sagaMiddleware.run(watchSearchSagas);
/* DISPATCH ACTION1 */
const action1 = searchSlice.actions.onChangeText("foo");
store.dispatch(action1);
/* IMMEDIATELY DISPATCH ACTION2. IT WILL CANCEL ACTION1'S SIDE EFFECTS
SEE THE CONSOLE */
const action2 = searchSlice.actions.onChangeCaseSensitive(true);
store.dispatch(action2);
In the example above, searchChanges
is a selectorChannel
. It tracks differences in the state provided by getSearchChanges
. It's provided to takeLatest
which will trigger handleSearchChanges
when it detects changes. Since it's provided as a channel, any takeX
effect can be used. takeLatest
will also cancel any handleSearchChanges
side-effects that are still executing.
useSaga
Coming Soon
Monitors
Coming Soon
Prior Art
- rxjs - Requires learning new control flow semantics. I found it very complex for simple tasks.
- redux-saga - Preserves well known control flow semantics for async tasks
- selector-channel - https://github.com/redux-saga/redux-saga/issues/1694 - opted for an implementation that compares the diff outside of sagas.
- more to come...
Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.