0.0.1-beta.6 • Published 3 years ago

dx-saga v0.0.1-beta.6

Weekly downloads
-
License
-
Repository
github
Last release
3 years ago

dx-saga

Warning: This package is in beta and subject to change frequently check back often for the latest.

npm version

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.

License

MIT

0.0.1-beta.6

3 years ago

0.0.1-beta.5

3 years ago

0.0.1-beta.4

3 years ago

0.0.1-beta.3

3 years ago

0.0.1-beta.2

3 years ago

0.0.1-beta.1

3 years ago