npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@teroneko/redux-saga-promise

v3.0.1

Published

Create actions that return promises, which are resolved or rejected by a redux saga

Downloads

4,125

Readme

@teroneko/redux-saga-promise

Use promises in redux sagas with TypeScript-first approach

Prologue

This projects aims to create promise actions (where each action has its deferred promise after surpassing the middleware) that are able to be fullfilled or rejected in redux-saga-manner.

Initially forked from @adobe/redux-saga-promise but completelly revamped to use createAction from @reduxjs/toolkit to support TypeScript.

Table of Contents

Overview

  • Redux middlware, namely promiseMiddleware, used to transform a promise-action-marked action (not promise action yet) to a fully qualified promise action that owns a deferred promise to allow a deferred fulfillment or rejection.
  • Saga helpers namely
    • implementPromiseAction() to resolve and reject by passing either a generator function, an asnyc function or simply a function where the return value is used to fulfill the deferred promise inside the promise action,
    • resolvePromiseAction() to resolve-only the deferred promise inside the promise action and
    • rejectPromiseAction() to reject-only the deferred promise inside the promise action.
  • Lifecyle actions namely
    • promiseAction.trigger (same instance like promiseAction) created by you (via promiseAction()) and dispatched against the redux store,
    • promiseAction.resolved created after the deferred promise has been resolved and
    • promiseAction.rejected created after the deferred promise has been rejected
    • can be used in reducers or wherever you would need these actions.
  • TypeScript helper types

Installation

npm install @teroneko/redux-saga-promise

Promise middleware integration

You must include include promiseMiddleware in the middleware chain, and it must come before sagaMiddleware:

import { applyMiddleware, createStore } from "redux"
import { promiseMiddleware }            from "@teroneko/redux-saga-promise"
import createSagaMiddleware             from "redux-saga"

// ...assuming rootReducer and rootSaga are defined
const sagaMiddleware = createSagaMiddleware()
const store          = createStore(rootReducer, {}, applyMiddleware(promiseMiddleware, sagaMiddleware))
sagaMiddleware.run(rootSaga)

Promise action creation

Use one of following method to create a promise action.

Promise action with type

Create a promise action (creator) with action-type.

import { promiseActionFactory } from "@teroneko/redux-saga-promise"

// creates a promise action with only a type (string)
const actionCreator = promiseActionFactory<resolve_value_type_for_promise>().create(type_as_string)
const action = promiseActionFactory<resolve_value_type_for_promise>().create(type_as_string)()
// equivalent to
const actionCreator = createAction(type_as_string)
const action = createAction(type_as_string)()

Promise action with type and payload

Create a promise action (creator) with action-type and payload.

import { promiseActionFactory } from "@teroneko/redux-saga-promise"

// creates a promise action with type (string) and payload (any)
const actionCreator = promiseActionFactory<resolve_value_type_for_promise>().create<payload_type>(type_as_string)
const action = promiseActionFactory<resolve_value_type_for_promise>().create<payload_type>(type_as_string)({} as payload_type) // "as payload_type" just to show intention
// equivalent to
const actionCreator = createAction<payload_type>(type_as_string)
const action = createAction<payload_type>(type_as_string)({} as payload_type) // "as payload_type" just to show intention

Promise action with type and payload creator

Create a promise action (creator) with a action-type and an individial payload (creator).

import { promiseActionFactory } from "@teroneko/redux-saga-promise"

// creates a promise action with type (string) and payload creator (function)
const actionCreator = promiseActionFactory<resolve_value_type_for_promise>().create(type_as_string, (payload: payload_type) => { payload })
const action = promiseActionFactory<resolve_value_type_for_promise>().create(type_as_string, (payload: payload_type) => { payload })({} as payload_type) // "as payload_type" just to show intention
// equivalent to
const actionCreator = createAction(type_as_string, (payload: payload_type) => { payload })
const action = createAction(type_as_string, (payload: payload_type) => { payload })({} as payload_type) // "as payload_type" just to show intention

Promise action await

Await a promise action after it has been dispatched towards the redux store.

:warning: Keep in mind that the action is not awaitable after its creation but as soon it surpassed the middleware!

// Internally all members of the promiseAction (without
// promise capabilities) gets assigned to the promise.
const promise = store.dispatch(promiseAction());
const resolvedValue = await promise;

Feel free to use then, catch or finally on promise.

Promise action fulfillment or rejection

Either you use

because it is up to you as the implementer to resolve or reject the promise"s action in a saga.

implementPromiseAction

The most convenient way! You give this helper a saga function which it will execute. If the saga function succesfully returns a value, the promise will resolve with that value. If the saga function throws an error, the promise will be rejected with that error. For example:

import { call, takeEvery }        from "redux-saga/effects"
import { promises as fsPromises } from "fs"
import { implementPromiseAction } from "@teroneko/redux-saga-promise"

import promiseAction from "./promiseAction"

//
// Asynchronously read a file, resolving the promise with the file"s
// contents, or rejecting the promise if the file can"t be read.
//
function * handlePromiseAction (action) {
  yield call(implementPromiseAction, action, function * () {
    // 
    // Implemented as a simple wrapper around fsPromises.readFile.
    // Rejection happens implicilty if fsPromises.readFile fails.
    //
    const { path } = action.payload
    return yield call(fsPromises.readFile, path, { encoding: "utf8" })
  })
}

export function * rootSaga () {
  yield takeEvery(promiseAction, handlePromiseAction)
})

If you call implementPromiseAction() with a first argument that is not a promise action, it will throw an error (see Argument Validation below).

resolvePromiseAction

Sometimes you may want finer control, or want to be more explicit when you know an operation won"t fail. This helper causes the promise to resolve with the passed value. For example:

import { call, delay, takeEvery } from "redux-saga/effects"
import { resolvePromiseAction }   from "@teroneko/redux-saga-promise"

import promiseAction from "./promiseAction"

//
// Delay a given number of seconds then resolve with the given value.
//
function * handlePromiseAction (action) {
  const { seconds, value } = action.payload
  yield delay(seconds*1000)
  yield call(resolvePromiseAction, action, value)
}

function * rootSaga () {
  yield takeEvery(promiseAction, handlePromiseAction)
})

If you call resolvePromiseAction() with a first argument that is not a promise action, it will throw an error (see Argument Validation below).

rejectPromiseAction

Sometimes you may want finer control, or want to explicitly fail without needing to throw. This helper causes the promise to reject with the passed value, which typically should be an Error. For example:

import { call, takeEvery }     from "redux-saga/effects"
import { rejectPromiseAction } from "@teroneko/redux-saga-promise"

import promiseAction from "./promiseAction"

//
// TODO: Implement this!   Failing for now
//
function * handlePromiseAction (action) {
  yield call(rejectPromiseAction, action, new Error("Sorry, promiseAction is not implemented yet")
}

function * rootSaga () {
  yield takeEvery(promiseAction, handlePromiseAction)
})

If you call rejectPromiseAction() with a first argument that is not a promise action, it will throw an error (see Argument Validation below).

Promise action's reducable lifecycle actions

Commonly you want the redux store to reflect the status of a promise action: whether it"s pending, what the resolved value is, or what the rejected error is.

Behind the scenes, promiseAction = promiseActionFactory().create("MY_ACTION") actually creates a suite of three actions:

  • promiseAction.trigger: An alias for promiseAction, which is what you dispatch that then creates the promise.

  • promiseAction.resolved: Dispatched automatically by promiseMiddleware when the promise is resolved; its payload is the resolved value of the promise

  • promiseAction.rejected: Dispatched automatically by promiseMiddleware when the promise is rejected; its payload is the rejection error of the promise

You can easily use them in handleActions of redux-actions or createReducer of @reduxjs/toolkit:

import { handleActions } from "redux-actions"
import promiseAction from "./promiseAction"

//
// For the readFile wrapper described above, we can keep track of the file in the store
//
export const reducer = handleActions({
    [promiseAction.trigger]:  (state, { payload: { path } }) => ({ ...state, file: { path, status: "reading"} }),
    [promiseAction.resolved]: (state, { payload: contents }) => ({ ...state, file: { path: state.file.path, status: "read", contents } }),
    [promiseAction.rejected]: (state, { payload: error })    => ({ ...state, file: { path: state.file.path, status: "failed", error } }),
  }, {})

Promise action caveats in sagas

In the sagas that perform your business logic, you may at times want to dispatch a promise action and wait for it to resolve. You can do that using redux-saga"s putResolve Effect:

const result = yield putResolve(myPromiseAction)

This dispatches the action and waits for the promise to resolve, returning the resolved value. Or if the promise rejects it will bubble up an error.

Caution! If you use put() instead of putResolve(), the saga will continue execution immediately without waiting for the promise to resolve.

Argument validation

To avoid accidental confusion, all the helper functions validate their arguments and will throw a custom Error subclass ArgumentError in case of error. This error will be bubbled up by redux-saga as usual, and as usual you can catch it in a saga otherwise it will will bubble up to the onError hook. If you want to, you can test the error type, e.g.:

import { applyMiddleware, compose, createStore } from "redux"
import { ArgumentError, promiseMiddleware }      from "@teroneko/redux-saga-promise"
import createSagaMiddleware                      from "redux-saga"

// ...assuming rootReducer and rootSaga are defined
const sagaMiddleware = createSagaMiddleware({ onError: (error) => {
  if (error instanceof ArgumentError) {
    console.log("Oops, programmer error! I called redux-saga-promise incorrectly:", error)
  } else {
    // ...
  }
})
const store = createStore(rootReducer, {}, compose(applyMiddleware(promiseMiddleware, sagaMiddleware)))
sagaMiddleware.run(rootSaga)

Additionally, all the helper functions will throw a custom Error subclass ConfigurationError if promiseMiddleware was not properly included in the store.

TypeScript helpers

Types

promiseAction.types does not really exist. It only exists as TypeScript-type to make use of typeof:

const promiseAction = promiseActionFactory<number>().create("MY_ACTION");

declare const type_of_promise_returned_when_surpassing_promise_middleware: typeof promiseAction.types.promise;
declare const type_of_resolved_value_from_promise_of_promise_action: typeof promiseAction.types.resolveValue;

declare const type_of_trigger_action_that_got_created_from_the_action_creator: typeof promiseAction.types.triggerAction;
declare const type_of_resolved_action_that_got_created_from_the_action_creator: typeof promiseAction.types.resolvedAction;
declare const type_of_rejected_action_that_got_created_from_the_action_creator: typeof promiseAction.types.rejectedAction;

Sagas

redux-saga cannot infer the parameters and return type of promiseAction correctly when using the call effect or equivalent, so you can use the pre-typed sagas:

const { implement, resolve, reject } = promiseAction.sagas;

// Instead of this...
call(implementPromiseAction, promiseAction(), () => 2);
// ... use this for better TypeScript support:
call(promiseAction.sagas.implement, promiseAction(), () => 2);

Contributing

Build and test

package.json defines the following scripts:

  • npm build: transpiles the source, placing the result in dist/src/index.js
  • npm test: builds, and then runs the test suite.

The tests are written using ts-jest;

Licensing

This project is licensed under the MIT License. See LICENSE for more information.