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

create-async-saga

v2.1.13

Published

Like readux-toolkit's createAsyncThunk, but for saga

Downloads

218

Readme

create-async-saga

For those that use both redux-toolkit and redux-saga, it is like createAsyncThunk, but using generator instead of asynchronous callback.

createAsyncSaga accepts a Redux action type string and generator function. It generates lifecycle action types based on the action type prefix that you pass in, and returns an object that allow to

  • dispatch an action to trigger your function
  • update your state using a slice

Getting started

!! You must have redux-toolkit & redux-saga installed and configured

You want to load a user using an api:

interface User {
  userId: number,
  name: string;
}

const fetchUserApi = (userId: number): User => ({ userId, name: "foo" });

Call createAsyncSaga and provide a generator that call the API:

export const fetchUser = createAsyncSaga(
  'users/fetch',
  function* (userId: number) {
    return yield call(fetchUserApi, userId)
  }
); 

fetchUser object contains action and it's type, the action to execute the saga. It has to be watched and dispatched

  • to watch:
export function* watchFetchUser() {
  yield takeEvery(fetchUser.actionType, fetchUser.asyncSaga);
}
// Don't forget to add this to the saga middleware: sagaMiddleware.run(watchFetchUser) 
  • to dispatch:
dispatch(fetchUser.action(1))
  • you can watch you own action:
export function* watchFetchUser() {
  yield takeEvery(myOwnAction, fetchUser.asyncSaga);
}

CreateAsyncSaga dispatch a pending action when the saga start, a fulfilled action when the saga is sucessfull, and a rejected action when saga failed. Like with createAsyncThunk, you write your own reducer logic using those actions:

createSlice({
  name: 'user',
  initialState: { ... },
  reducers: { ... },
  extraReducers(builder) {
    builder.addCase(
      fetchUser.pending,
      (state) => { 
        state.fetchStatus = "pending"; 
      }
    );
    builder.addCase(
      fetchUser.fulfilled,
      (state, action) => {
        state.fetchStatus = "fulfilled";
        state.user = action.payload;
      }
    );
    builder.addCase(
      fetchUser.rejected,
      (state) => {
        state.fetchStatus = "rejected"; 
        // action.payload contains error information
      }
    );
  }
});

API

Release 2.0.0 includes requestId in actions. This implies a modification in actions creator interface, which is a breaking change. This impact only code that creates action by itself (which is probably test including full saga if any).

function createAsyncSaga<Returned, Arg>(typePrefix: string, payloadCreator: PayloadCreator<Returned, Arg>, options?: AsyncSagaOptions<Arg>)

type PayloadCreator<Returned, Arg> = (arg: Arg) => Generator<any, Returned, any>;

interface AsyncSagaOptions<Arg> {
  condition?: Condition<Arg>,
  dispatchConditionRejection?: boolean,
}

type Condition<Arg> = (arg: Arg) => Generator<any, boolean, any>;
  • Returned is the return type of the playlod generator and Arg is the arguments type of the payload creator.

  • typePrefix: prefix for generated Redux action type constants.

    When typePrefix is users/fetch actions types are users/fetch, users/fetch/pending, users/fetch/fulfilled and users/fetch/rejected.

  • payloadCreator: a generator that returns the payload to be put in fulfilled action. It can yield any effects it needs.

      function* (userId: string) {
        const user: User = yield call(fetchUser, userId); // yield an effect
        return user; // returns the result
      },

    A payloadCreator can receive an arg. If you need to pass in multiple values, pass them together in an object when you dispatch the action.

  • options: an object with the following optional fields:

    • condition: a generator that can be used to skip execution of the payload creator. It can yield any effects but it returns a boolean. It receives arg payload creator as argument.
    • dispatchConditionRejection: by default if condition returns false, nothing is dispatched. When dispatchConditionRejection is set to true a "rejected" action is dispatched with meta.condition set to true.

Actions

Actions are made of action itself, with is dispatched to trigger the async execution, and of createAsyncThunk's lifecycle actions: pending, fulfilled and rejected.

  • action:
    {
      type: `typePrefix`, // the typePrefix is given to createAsyncSaga
      payload: arg, // object that contains the paylod creator arguments 
      meta: {
        requestId: string // a uniqe id generatted for each execution
      }
    }
  • pending:
    {
      type: `typePrefix`/pending, // the typePrefix is given to createAsyncSaga
      payload: undefined, // object that contains the paylod creator arguments 
      meta: {
        arg, // object that contains the paylod creator arguments
        requestId // id generated 
      }
    }
  • fulfilled:
    {
      type: `typePrefix`/fulfilled, // the typePrefix is given to createAsyncSaga
      payload: returned, // payload creator returned value 
      meta: {
        arg, // object that contains the paylod creator arguments
        requestId // id generated 
      }
    }
  • rejected:
    {
      type: `typePrefix`/rejected, // the typePrefix is given to createAsyncSaga
      payload: error, // a SerializedError made from what the payload creator thrown 
      meta: {
        arg, // object that contains the paylod creator arguments
        requestId // id generated
        condition // true if rejected has been dispatched because condition has return false
      }
    }

Testing

One can test the payload generator only or the saga returned by createAsyncSaga, although the full saga requires more stuff. In both case, test can be written using any Saga tests receipes.

A generator payload test (using redux-saga-test-plan):

it('Test payload generator', () => {
  const user: User = {
    id: '123',
    name: "John doe",
  };
  testSaga(fetchUser, user.id)
    .next()
    .call(fetchUser, user.id)
    .next(user)
    .returns(user);
});

A full saga test (still using redux-saga-test-plan):

const fetchUserSaga = createAsyncSaga(
  'users/fetch',
  fetchUser
);

it('Test full saga', () => {
  const user: User = {
    id: '123',
    name: "John doe",
  };
  const action = fetchUserSaga.action(user.id);
  const requestId = action.meta.requestId;
  testSaga(fetchUserSaga.asyncSaga, action)
    .next()
    .put(fetchUserSaga.pending(user.id, requestId))
    .next()
    .call(fetchUser, user.id)
    .next(user)
    .put(fetchUserSaga.fulfilled(user.id, requestId, user))
    .next()
    .isDone();
});

Known limitations & issues

This lib is new, and is missing some advanced functionalities (they will be added in the coming releases):

  • ~~dispatchConditionRejection~~ is missing in options Fixed in release 2.1.0
  • When the async saga is cancelled a rejected action with meta.aborted===true should be thrown
  • ~~requestId is missing in meta~~ Fixed in release 2.0.0
  • ~~error may not be a real SerializedError~~ Fixed in release 1.0.2