Giter Site home page Giter Site logo

redux-saga / redux-saga Goto Github PK

View Code? Open in Web Editor NEW
22.5K 22.5K 2.0K 9.18 MB

An alternative side effect model for Redux apps

Home Page: https://redux-saga.js.org/

License: MIT License

JavaScript 82.37% TypeScript 16.18% CSS 0.40% SCSS 1.05%
effects middleware redux redux-saga sagas

redux-saga's Introduction

Redux Logo Landscape

redux-saga

npm version CDNJS npm Discord Shield OpenCollective OpenCollective

redux-saga is a library that aims to make application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage, more efficient to execute, easy to test, and better at handling failures.

The mental model is that a saga is like a separate thread in your application that's solely responsible for side effects. redux-saga is a redux middleware, which means this thread can be started, paused and cancelled from the main application with normal redux actions, it has access to the full redux application state and it can dispatch redux actions as well.

It uses an ES6 feature called Generators to make those asynchronous flows easy to read, write and test. (if you're not familiar with them here are some introductory links) By doing so, these asynchronous flows look like your standard synchronous JavaScript code. (kind of like async/await, but generators have a few more awesome features we need)

You might've used redux-thunk before to handle your data fetching. Contrary to redux thunk, you don't end up in callback hell, you can test your asynchronous flows easily and your actions stay pure.

Getting started

Install

$ npm install redux-saga

or

$ yarn add redux-saga

Alternatively, you may use the provided UMD builds directly in the <script> tag of an HTML page. See this section.

Usage Example

Suppose we have a UI to fetch some user data from a remote server when a button is clicked. (For brevity, we'll just show the action triggering code.)

class UserComponent extends React.Component {
  ...
  onSomeButtonClicked() {
    const { userId, dispatch } = this.props
    dispatch({type: 'USER_FETCH_REQUESTED', payload: {userId}})
  }
  ...
}

The Component dispatches a plain Object action to the Store. We'll create a Saga that watches for all USER_FETCH_REQUESTED actions and triggers an API call to fetch the user data.

sagas.js

import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import Api from '...'

// worker Saga: will be fired on USER_FETCH_REQUESTED actions
function* fetchUser(action) {
  try {
    const user = yield call(Api.fetchUser, action.payload.userId)
    yield put({ type: 'USER_FETCH_SUCCEEDED', user: user })
  } catch (e) {
    yield put({ type: 'USER_FETCH_FAILED', message: e.message })
  }
}

/*
  Starts fetchUser on each dispatched `USER_FETCH_REQUESTED` action.
  Allows concurrent fetches of user.
*/
function* mySaga() {
  yield takeEvery('USER_FETCH_REQUESTED', fetchUser)
}

/*
  Alternatively you may use takeLatest.

  Does not allow concurrent fetches of user. If "USER_FETCH_REQUESTED" gets
  dispatched while a fetch is already pending, that pending fetch is cancelled
  and only the latest one will be run.
*/
function* mySaga() {
  yield takeLatest('USER_FETCH_REQUESTED', fetchUser)
}

export default mySaga

To run our Saga, we'll have to connect it to the Redux Store using the redux-saga middleware.

main.js

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import reducer from './reducers'
import mySaga from './sagas'

// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount it on the Store
const store = createStore(reducer, applyMiddleware(sagaMiddleware))

// then run the saga
sagaMiddleware.run(mySaga)

// render the application

Documentation

Translation

Using umd build in the browser

There is also a umd build of redux-saga available in the dist/ folder. When using the umd build redux-saga is available as ReduxSaga in the window object. This enables you to create Saga middleware without using ES6 import syntax like this:

var sagaMiddleware = ReduxSaga.default()

The umd version is useful if you don't use Webpack or Browserify. You can access it directly from unpkg.

The following builds are available:

Important! If the browser you are targeting doesn't support ES2015 generators, you must transpile them (i.e. with babel plugin) and provide a valid runtime, such as the one here. The runtime must be imported before redux-saga:

import 'regenerator-runtime/runtime'
// then
import sagaMiddleware from 'redux-saga'

Building examples from sources

$ git clone https://github.com/redux-saga/redux-saga.git
$ cd redux-saga
$ yarn
$ npm test

Below are the examples ported (so far) from the Redux repos.

Counter examples

There are three counter examples.

counter-vanilla

Demo using vanilla JavaScript and UMD builds. All source is inlined in index.html.

To launch the example, open index.html in your browser.

Important: your browser must support Generators. Latest versions of Chrome/Firefox/Edge are suitable.

counter

Demo using webpack and high-level API takeEvery.

$ npm run counter

# test sample for the generator
$ npm run test-counter

cancellable-counter

Demo using low-level API to demonstrate task cancellation.

$ npm run cancellable-counter

Shopping Cart example

$ npm run shop

# test sample for the generator
$ npm run test-shop

async example

$ npm run async

# test sample for the generators
$ npm run test-async

real-world example (with webpack hot reloading)

$ npm run real-world

# sorry, no tests yet

TypeScript

Redux-Saga with TypeScript requires DOM.Iterable or ES2015.Iterable. If your target is ES6, you are likely already set, however, for ES5, you will need to add it yourself. Check your tsconfig.json file, and the official compiler options documentation.

Logo

You can find the official Redux-Saga logo with different flavors in the logo directory.

Redux Saga chooses generators over async/await

A few issues have been raised asking whether Redux saga plans to use async/await syntax instead of generators.

We will continue to use generators. The primary mechanism of async/await is Promises and it is very difficult to retain the scheduling simplicity and semantics of existing Saga concepts using Promises. async/await simply don't allow for certain things - like i.e. cancellation. With generators we have full power over how & when effects are executed.

Backers

Support us with a monthly donation and help us continue our activities. [Become a backer]

Sponsors

Become a sponsor and get your logo on our README on Github with a link to your site. [Become a sponsor]

License

Copyright (c) 2015 Yassine Elouafi.

Licensed under The MIT License (MIT).

redux-saga's People

Contributors

abdulkabia avatar acdvs avatar aikoven avatar andarist avatar baisang avatar bit3725 avatar camflan avatar cef62 avatar coryhouse avatar djebbz avatar eloytoro avatar erikvold avatar gilbsgilbs avatar github-actions[bot] avatar jihchi avatar jscinoz avatar karland avatar kuy avatar michaelgilley avatar mshustov avatar mxstbr avatar neighborhood999 avatar neurosnap avatar pbadenski avatar secobarbital avatar seregase avatar shinima avatar tangruixing avatar thezanke avatar yelouafi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

redux-saga's Issues

Put actions as one batch

Is this possible to put several actions in batch else each put leads to re-render of components?

put([...actions]) or put(action1, action2, action3, ...) would be great.

Inconsistent Blocking Actions

The first example in the README seems to be broken. It is supposed to wait for INCREMENT_ASYNC actions, then delay them by 1 second, and then pass an INCREMENT_COUNTER action on, but it misses any extra actions fired in that 1 second delay.

import { take, put } from 'redux-saga'
// sagas/index.js

function* incrementAsync() {

  while(true) {

    // wait for each INCREMENT_ASYNC action  
    const nextAction = yield take(INCREMENT_ASYNC)

    // delay is a sample function
    // return a Promise that resolves after (ms) milliseconds
    yield delay(1000)

    // dispatch INCREMENT_COUNTER
    yield put( increment() )
  }

}

export default [incrementAsync]

This problem exists in any saga that makes a blocking call (delay, call, join, put etc.). My initial recommendation would therefore be: If you don't want race conditions, you must never make sagas that contain blocking calls. This makes sagas pretty much useless though, unless you work on the basis of always having a top level saga that looks like while (true) yield fork(yield take('a'));.

My recommendation is to do one of two things:

Buffer actions

One option is to buffer actions so that take just reads the next item of the buffer. This has a couple of issues:

  1. It prevents parallelism. In the above example, doing dispatch(incrementAsync());dispatch(incrementAsync());dispatch(incrementAsync()); would result in a 3 second delay (1 second * 3) rather than all running in parallel and taking about 1 second.
  2. You don't know in advance when or if a saga will attempt to take a given action type, so you are forced to buffer all actions indefinitely.

Subscribe at the top level

I imagine that 90% of sagas look like:

export default function* mySaga() {
  while (true) {
    let action = yield take(MY_ACTION_NAME);
    // ... business logic here ...
  }
}

So, with that in mind, why not just have that be re-written as:

function* mySaga(action) {
  // ... business logic here ...
}
export default createSaga(MY_ACTION_NAME, mySaga);

Saga could then dispatch actions matching MY_ACTION_NAME in parallel.

What you loose seems to be the ability to "hide" state inside sagas, but I don't think you should be putting state inside your sagas. Keep your state in the store (updated via reducers).

Hot reducer loading is broken in examples/real-world

Repro: start the server, change the default error message to 'test'. Redux complains about an invalid reducer.

The problem is that Babel 6 changed how default exports are stored:

// a.js
export default 'hello'

// b.js, Babel 5
const hi = require('./a')
assert(hi === 'hello')

// b.js, Babel 6
const hi = require('./a')
assert(hi !== 'hello')
assert(hi.default === 'hello')

So you need to add .default here in order to require the reducer itself.

Saga ? Why not CSP ?

Hello,

Honest question here. I saw the repo through @Gaeron's tweet, read the README (briefly, I admit), and wondered why not simply using CSP ? In ES6 they use generators like Sagas does, and seem to already provide everything Saga need to handle async scenarios (take, takeAsync, put, putAsync, wait/delay, a way to wrap Node/JS-style callbacks). In fact, the CSP paradigm can handle any async scenarios (along with the Actor model). Here's a repo with a good implementation.

Sagas Yielding to each other asycnronously

Hey There,
Love Sagas and have been using them for quite a few tasks. There is one thing I have struggled with and I am sure it is just right there and I can't see it, but that has to do with the idea of yielding to each other asynchronously. Mainly this is because a saga is registering an event listener which makes the given callback whenever data is available.

I have a feeling this may be a scope issue with callbacks, etc since you can't use yield in a callback function, but was hoping there was a way to accomplish something like this. I know observables tend to be the solution for this in many cases.

I am utilizing a self-built solution right now which does appear to work which simply dispatches to the store in a separate function, but I feel this could be accomplished in a better way.

Basically, my goal is to create the following workflow (correct me if I am way off-base with this):

  1. Listen for an event "REGISTER_EVENT_LISTENER"
  2. Fork and create a new listener which yields to the event listener function
  3. Event Listener awaits the event, processes the data, and makes all the appropriate updates to the store.
  4. Allow simple cancellation of any event listener.

I think with standard generators this can be achieved by passing the "next" function as the callback which responds to the yield which is within a loop. It isn't clear from the documentation if something like this is supported. Would love to get an idea for how this might work... for arguments sake lets say we wanted to bind a key press to a saga so that any time it was pressed, the yield would resolve.

In my case I am using a streaming database to receive updates from other parts of the application ala:

listener.on(type, (received) => {
     // data received!
})

Sorry if I missed something in the documentation describing how this may work.

question: testing

This issn't really an issue, I'm just trying to wrap my head around certain aspects of testing.

How do I use a yielded value inside a generator to continue on with tests? Consider:

    function* watchDoSomething() {
        while (true) {
            const { thing } = yield io.take(actions.DO_SOMETHING)
            const fetch = yield io.fork(fetchSomething(thing.id))
        }
    }


...
        it('should wait for the next DO_SOMETHING', function() {
            sut.next().value.should.eql(io.take(actions.DO_SOMETHING))
        })
        it('should dispatch a fetch request', function() {
            sut.next().value
            sut.next().value.should.eql(io.fork(creators.fetchSomething)) 
            // errors with 'cannot read property thing of undefined' : attempting to destructure the undefined
            // result from yield io.take
        })
        it('should invoke fetch with the id', function() {
            sut.next().value
            sut.next().value
           // how do I test this last piece?
            sut.next().value.should.eql( ... )
        })

How do you simultaneously test that DO_SOMETHING is called and return a value on that thing? As far as the application is concerned, thing already has an id embedded on the action... but again, I'm not sure how to simulate this in the world of generators.

doc improvements for saga vs thunk, handling simple cases

This seems like some low hanging fruit that would aid newcomers.

When reading the docs it's not immediately clear to me how to use a saga in "the simplest case". By simplest case I mean there is a view button that triggers a callback, and that callback has async behavior. In a redux-thunk implementation, that callback would be a bound action creator with some middleware to handle the async nature of the callback:

<button onClick={this.props.login} />

...

mapDispatchToProps(dispatch) {
    return bindActionCreators(actionCreators, dispatch)
}

...

let actionCreators = {
    login() {
        return (dispatch, getState) => {
            dispatch({
                type: LOGIN_REQUEST
            })

            return xhr({
                method: 'POST'
                , url: api.login
            })
            .then((res) => {
                if (res.statusCode === 200) {
                    dispatch({
                        type: LOGIN_SUCCESS
                    })
                }
            })
        }
    }
}

The docs say redux-saga is a replacement/alternative for redux-thunk or other middlewares, and neatly explains how sagas are a much more powerful construct, especially when dealing with longer running sequences of events. However, it's not immediately clear whether the use case of "one event, one action" also falls under a saga's domain, or how we might implement that case. This is how I would do it, but I'm open to suggestions:

let actionCreators = {
    login({ username, password }) {
        return {
            username
            , password
            , type: LOGIN
        }
    }
    , loginRequest() {
        return {
            type: actions.DO_LOGIN_REQUEST
        }
    }
    , loginError() {
        return {
            type: actions.DO_LOGIN_ERROR
        }
    }
    , loginFailure() {
        return {
            type: actions.DO_LOGIN_FAilURE
        }
    }
    , loginSuccess(response) {
        return {
            response
            , type: actions.DO_LOGIN_SUCCESS
        }
    }
}

function* watchForLogin() {
    while (true) {
        const { username, password } = yield take(actions.LOGIN)
        yield call(login, { username, password })
    }
}

function* login({ username, password }) {
    yield put(actionCreators.loginRequest())
    let { statusCode, body } = yield call(doLogin, { username, password })

    if (statusCode > 499) {
        yield put(actionCreators.loginError())
        return
    }

    if (statusCode > 399) {
        yield put(actionCreators.loginFailure())
        return
    }

    yield put(actionCreators.loginSuccess(body))
}

function* doLogin({ username, password }) {
    const b64 = btoa(`${username}:${password}`)
    return xhr({
        method: 'POST'
        , url: api.login
        , headers: {
            'authorization': `Basic ${b64}`
        }
    })
}

First, is this the "right way" to make a one-to-one, or is there a cleaner approach than having an explicit "start this saga" action creator callback? Second, do you think this is something that should appear in the docs?

reusable components, distinguishing events?

I'm curious how one might utilize reusable components or dynamic components that involve a saga.

Consider if you had a date-picker on the page, perhaps a start date and end date for some configuration settings. That component might be pretty well isolated, perhaps even reusable across different applications.

parentAReducer(state, action) {
    ...
    case picker.PICK_DATE:
        state.startDate = action.pickedDate 

    ...
}

parentBReducer(state, action) {
    ...
    case picker.PICK_DATE:
        state.endDate = action.pickedDate 

    ...
}

Assuming you have already figured out a way to direct distinct/unique messages to the appropriate implementing reducer of the reusable component (perhaps attaching an id to the action, or some sort of sophisticated messaging bus) how can that distinction be made inside the saga?

function* watchForEndDate() {
    while (true) {
        const {pickedDate} = yield io.take(picker.PICK_DATE)

        // ... ut oh, is this start date or end date? 
    }
}

The problem shifts slightly when dealing with not just reusable components, but dynamic reusable components, because suddenly you don't know until runtime how many identical actions you have to sift through to find the right one.

function* watchForEndDate() {
    while (true) {
        const {pickedDate} = yield io.take(picker.PICK_DATE)

        // ... ut oh, is this start date or end date for our "widget A" or "widget B" or "widget A1" or "widget B1"? 
    }
}

It's very plausible this is a user-land concern, and not a library concern, but I would still be interested in hearing some thoughts others might have.

DevTools for Sagas

Redux DevTools currently leave a lot to be desired.
Mainly because they don't track async interactions at all.

It's impossible to say which component caused which async action, which in turn caused other async actions, which in turn dispatched some regular actions.

However sagas seem different. They make side effect control flow more explicit and don't execute side effects unless the caller demands so. If we wrapped top level functions such as take(), call(), put(), fork(), into tracking functions, we could potentially build a graph of saga control flow and trace action history through the sagas that generated it. This is similar to Cerebral debugger UI which unlike ours can trace which Signal was caused by which other Signal.

The developer wins in my opinion could be huge here. If this is possible to build we need to build it. At first as a proof of concept with console logging, and later with a UI, possibly as an extension to Redux DevTools.

What do you think? Am I making sense here? Have I missed something that makes this infeasible, not useful, or hard?

redux-saga + webpack + uglifyjs: Saga must be a Generator function

First I would like to say that I am exiting about redux-saga.

I faced problem with redux-saga + webpack + uglifyjs combination in production build. For some reason sagas are "too well" uglified and don't pass the check utils.is.generator(saga). In result it throws and an error "Saga must be a Generator function"

I found a workaround. Just simple use such options:
new webpack.optimize.UglifyJsPlugin({
mangle: {except: ['GeneratorFunction'] }
})

Hope it helps someone. Is there any better way to avoid such errors?

SagaCancellationException should be an instance of Error?

I have a picky test framework that complains when non-errors are thrown, i.e.

throw 'error' // `'error' was thrown, throw an Error :)`
// ...
iterator.throw('error') // same as above

I tried using new SagaCancellationException(), but that causes the same message. I attempted to extend Error so that SagaCancellationException instanceof Error === true:

export class SagaCancellationException extends Error {
  constructor(type, saga, origin) {
    super()
    this.type = type
    this.saga = saga
    this.origin = origin
  }
}

But I didn't get past the test cases. I'll mess with this later. I may be extending the Error class incorrectly, or my test cases may be completely wrong. I've never tested catch statements in generators before, so I'm leaning towards the latter.

Saga:

export function* addAsync(value: number): any {
    try {
        yield call(delay, 1000);
        yield put(add(value));
    } catch (error) {
        if (error instanceof SagaCancellationException) {
            yield put(failure());
        }
    }
}

Test:

    it('addAsync saga cancellation', (done: MochaDone) => {
        const iterator: any = addAsync(66);

        assert.deepEqual(
            iterator.throw(new SagaCancellationException()).value,
            put(failure())
        );
    });

Server Side Rendering (Universal)

Do sagas handle server side rendering?

When on the server, there is a need to adapt async code into a synchronous flow, as we are handling a blocking server request that is waiting on some sort of data. If we're using React, we need to call ReactDOM.renderToString after we have our redux state populated. Often times that state is sourced from async sources, such as a REST API. So, we have to resolve those asynchronous processes back in the flow of the request.

I currently use Promises (and some custom data fetching dectorators) to handle this:

Promise.all(
  prefetchData(components, store)
).then(render);

That's a simplified form, but it's basically the same thing. My component decorator defines certain actions to send through Redux when calling prefetchData on it. It gets the promises from those actions (I use redux-promise currently) and bundles them up with Promise.all to let them be resolved before rendering.

Now, given a relatively complex saga, it looks like there isn't a sole promise to resolve. Is there, instead, some way we could watch for the completion of a set of sagas? That is, I could continue to dispatch my actions from my component decorators, followed by some call to the saga runner to wait for the generator functions we've invoked from those actions to finish. If so, I can use that as a means to get back to my synchronous flow and return a response to my blocking server request.

Dynamically starting sagas

This might have some overlap with with the runSaga idea mentioned in #13 (comment).

Currently all sagas need to be provided during startup, I am curious if it make sense to have the ability to dynamically start them.

After writing the example below I think the main use case would be larger apps that use code splitting, if you know all your sagas ahead of time its probably simpler to have watchers which fork based on dispatched actions (compatible with time travel).


For example if we only want the onBoarding saga to be running while in a specific section of our app it could be started/stopped with a container component:

componentWillMount() {
  this.onBoardingTask = runSaga(onBoarding, this.props.store);
}

componentWillUnmount() {
  cancelSaga(this.onBoardingTask);
}

Forked subtasks should be cancelled when parent is cancelled (?)

Imagine I have Task1 that forks Task2 that forks Task3

Now I cancel Task1, which cause a SagaCancellationException inside Task1.
But by default seems forked tasks Task2 and Task3 are not cancelled and can stay alive even if the parent Task1 is cancelled.

Is it supposed to be handled by redux-saga, or should I propagate the cancellation myself to forked subtasks?

With this code I successfully propagate the cancellation to subtasks but it requires additional boilerplate:

export default function* task1() {
  let subtasks = [];
  try {
    subtasks.push(yield fork(task2));
    subtasks.push(yield fork(task3));
    while ( true ) {
      yield take("SOME_EVENT_THAT_RESTART_SUBTASKS");
      yield subtasks.map(cancel);
      subtasks = [];
      subtasks.push(yield fork(task2));
      subtasks.push(yield fork(task3));
    }
  }
  catch(e) {
    if ( e instanceof SagaCancellationException ) yield subtasks.map(cancel);
    else throw e;
  }
}

Infinite loop when using take('*')

Yielding effect take('*') results in dispatching monitor action which can be again consumed by the same effect. Simplest way to reproduce:

function* saga() {
  while (true) {
    yield take('*');
  }
}

Does module run with redux-thunk?

I tried dispatching functions (for thunk) but nothing happened:

yield put(myAction())

If myAction returns a function (for thunk to consume), nothing is called. However, if myAction is an action object, myAction is called.

Unhandled promise rejection TypeError: Cannot read property 'response' of undefined

Hi. I'm having a lot of trouble trying to incorporate facebook login and using sagas to do it. In my sagas, I have:

function* loginUser(username) {
  yield put(actions.loginUser.request(username));
  const { response, error } = yield call(api.loginUser, username);
  if (!error) {
    yield put(actions.loginUser.success(username, response));
  } else {
    yield put(actions.loginUser.failure(username, response));
  }
}

and then my async call looks like:

function loginUser(username) {
  fetch('/auth/facebook')
    .then(function (response) {
      if (response.status >= 400) {
        throw new Error('Bad response from server');
      }
      return response;
    })
    .then(
      response => ({ response }),
      error => ({ error: error.message || 'Something shitty happened' })
    );
}

However, I keep getting a promise rejection error. The line const { response, error } = yield call(api.loginUser, username); is where it throws. I know that the response is coming back because i console.log it and it's there. But for some reason, the yield call is not able to read the response.

Would you happen to know if I'm doing anything wrong?

Proposal: Parametric sagas

I've run into following problem:
Suppose we have two sagas: one daemon, and one callable:

function *daemonSaga() {
  while (true) {
    const action = take(...);

    yield call(callableSaga, action.payload);
  }
}

function *callableSaga(params) {
  // ...
}

Now, suppose that callableSaga becomes dependent on state (in my particular case it depends on locale setting that is stored in the state). We could use getState() in daemonSaga and pass extra state data to callableSaga, but that's not very convenient: if there is another dependency in callableSaga, we'd have to change code of daemonSaga again; also, if there is another saga that calls callableSaga, we'd have to duplicate code for extracting state data. It seems that it has to be callableSaga's responsibility to get its state dependencies.

So my first approach was to just pass getState to callableSaga:

function *daemonSaga(getState) {
  // ...
    yield call(callableSaga, action.payload, getState);
  // ...
}

function *callableSaga(params, getState) {
  // ...
}

Although it still has some drawbacks: every saga that calls callableSaga must accept getState argument even if it doesn't have dependencies on state itself; if nested call level is longer then we have to pass getState from the very top.

Sounds familiar, isn't it? (Hint: react-redux).

There's another concern about getState argument for sagas: they become dependent on state shape which breaks encapsulation.

Proposal

  • Allow sagas to be connected to the store, similar to how React components are connected by react-redux. If function decorators were supported, it could look like this:
@connectSaga(state => /* extract params */)
function* connectedSaga(getParams) {
  // ...
}

Decorator returns regular saga with getState argument. Added benefit here is that we can decouple saga from state shape by using selectors.

  • Allow sagas to call connected sagas without having to pass getState:
function *saga() {
  yield run(connectedSaga);
}
  • Allow connected sagas to accept extra parameters. They can be handled in similar way as react-redux handles component props: merge them with result of selector:
@connectSaga(state => ({stateParam: ...}))
function* connectedSaga(getParams) {
  const {stateParam, extraParam} = getParams();
  // ...
}

function *saga() {
  yield run(connectedSaga, {extraParam: ...});
}

Subscribing to websocket events

Hi. I would really appreciate some clarification om how to handle my case.
Sorry for a bit lengthy explanation, but I'd like to describe every bit involved.

I have several sagas:

  1. XHR API middleware that takes the API request action, calls the API, waits for the promise to resolve, etc. Pretty standard and I have no issues with it. It uses an isolated "service" module to make the actual XHR calls.
  2. Websocket middleware that has 3 different sagas: connect, subscribe, emit. It uses an isolated service (which essentially wraps the socket.io-client lib). The connect method returns a promise (and works OK). The emit method is synchronous, works fine as well. The subscribe method is the one that baffles me. The issue here is that events from the socket can happen multiple times, so I cannot promisify this method. See below for how I use it.
  3. The actual "meat" saga that has to do the following: take the XHR success event for a specific resource (chat rooms list), subscribe to the websocket events for the new messages, and emit the subscription websocket event (that will actually put the current client into the listeners list for the given char rooms on the server).

Now here's how the latter saga looks:

export default function* myRooms(getState) {
  while (true) {
    yield take(MY_ROOMS_SUCCESS) // works OK

    yield put(subscribeSocket({ // it's sent and taken just fine in the other saga
      event: 'newMessage', // the name of the socket.io event to subscribe to
      actionType: MY_ROOMS_NEW_MESSAGE // the name of the action to dispatch
    }))

    const state = getState()
    const roomIds = ... // get the IDs of the chat rooms
    yield put(emitSocket({
      event: 'subscribe', // the name of the socket.io event to emit
      payload: roomIds
    }))
  }
}

Now for the socket subscription saga:

function* subscribeSocket(getState) {
  while (true) {
    const nextAction = yield take(SUBSCRIBE_SOCKET)
    const subscribeConfig = nextAction.payload
    const { event, actionType } = subscribeConfig
    const callback = (payload) => put({ type: actionType, payload }) // <-- here
    const unsibscribe = socket.subscribe(event, callback)
  }
}

So the subscription saga gets the socket event name to listen to, and the action type name to dispatch together with the event payload.
I can tell from the debugging that the callback is actually executed. As far as I understand the problem here is the improper put usage.

The question is: what is the proper usages of sagas in this case? I want to dispatch the actionType every time the callback is called.

Thanks in advance!

Saga misses multiple actions dispatched within the same event queue

Consider this example:

function* fnA() {
  while(true) {
    let {payload} = yield take('a')
    yield fork(someAction, payload)
  }
}

function* fnB() {
  yield put({type: 'a', payload: 1})
  yield put({type: 'a', payload: 2})
  yield put({type: 'a', payload: 3})
}

function* someAction(payload) {
  console.log(payload)
}

export default function* root() {
  yield [
    fork(fnA),
    fork(fnB)
  ]
}

The output I was expecting was 1, 2, 3, however the actual output is 1, 3 this is because the yield fork doesn't continue on the while loop until a subsequent tick. This can be fixed by changing fnA to:

function* fnA() {
  let forked = false
  while (!forked) {
    let {payload} = yield take('a')
    forked = yield fork(fnA)
    yield fork(someAction, payload)
  }
}

However this makes logging turn into a pyramid and feels like the wrong approach. Am I thinking about this incorrectly or could we somehow force the next tick after a fork call?

How to run sagas in loop

First of all thanks a lot for providing community with this nifty tool. I am able to reduce lot of corners around my app.

One thing I was wondering how to achieve is how to run sagas in loop. i.e.

forEach(result, (item) => {
  yield fork(api.endpoint, item);
});

I am getting following error while trying so

Uncaught (in promise) Error: https://localhost:3000/fuses/fuse-react-redux/sagas/index.js: Unexpected token (30:12)
  28 |     yield put(actions.requestNewItems());
  29 |     forEach(result, (item) => {
> 30 |       yield fork(api.endpoint, item)
     |             ^
  31 |     });

saga works if put outside of loop with single item. e.g.

yield fork(api.endpoint, item)

I am not sure if by convention it's not supposed to work this way.

Integrating with redux router

First of all thanks for putting this together, I really like the sagas idea. I have a question and don't know if this is the best place but here it goes anyway.

I'm trying to integrate with redux-router, the idea is to be able to take redux-router actions and put a new action if necessary.

import { pushState } from 'redux-router'
import ROUTER_DID_CHANGE from 'redux-router/lib/constants'
import * as actions from '../actionCreators'
// sagas/index.js

export function* watchNavigate() {
  let action = null
  while(action = yield take(ROUTER_DID_CHANGE)) {
    yield put(actions.show(action.payload.pathname)) //navigate
  }
}

//...

The main idea is to be able to react to a route change event like hitting the browser back button.

Is there any way to do it? I tried and all my actions can be takeen but none of the redux-router ones, I'm certainly missing something.

Thanks in advance.

Question: Authentication flow

I'm trying to implement user authentication with following requirements:

  • Sign-in can be triggered by user action
  • Auth token must be refreshed after some delay upon successful sign-in
  • If there is token stored in localStorage on app start then we must refresh it immediately

Here's my attempt:

function* authentication() {
  let refreshDelay = null;

  let token = JSON.parse(localStorage.getItem('authToken'));

  if (token) 
    refreshDelay = call(delay, 0);  // instant refresh

  while (true) {
    const {action} = yield race({
      action: take([SIGN_IN, SIGN_OUT]),
      delay: refreshDelay || call(waitForever)
    });
    refreshDelay = null;

    if (action && action.type === SIGN_OUT) {
      localStorage.removeItem('authToken');
      continue;
    }

    try {
      token = yield action == null ? auth.refreshToken(token) : auth.signIn();
      localStorage.setItem('authToken', JSON.stringify(token));
      yield put(authSuccess(token));
      refreshDelay = call(delay, token.expires_in);
    } catch (e) {
      localStorage.removeItem('authToken');
      yield put(authFailure(e));
    }
  }
}

This code works well, but I wonder if there is more elegant solution for handling refresh. Currently it's too difficult to track refreshDelay effect. I thought about extracting refresh to separate saga that waits for AUTH_SUCCESS action and then sets up a delay, but in this case I won't be able to cancel scheduled refresh if e.g. user signs out.

Time-travel and hot reloading

Suppose we have a saga to handle authentication of an user like in your example.

What bothers me is that I'm not sure the behavior would be correct when associated with time-travel debugging. I mean if the saga has an initial variable let userConnected = false, then the user connects so userConnected = true, and then we time travel back to the beginning. Here the saga will still have userConnected = true right?

But I'm not sure it's actually a problem as this project is a middleware and what I understand of Redux devtools is that during time travel the actions do not go through the middleware chain again so the saga would not change state.

What about hot reloading of Saga code? I'm not sure here it will work at all either but not sure it is really possible to do something nice about it right?

Async/Await

Hi there,

Just started going through the documentation and was thinking about showing examples with async/await abstraction, instead of generator syntax. Generator syntax is quite low level and it is too bad that developers not familiar with the concept risk getting a bit confused. Async/await is a lot easier to reason about.

Just a question, not a demand in any way :-)

How to take on either type?

How can I yield on the take of either types.TYPE_ONE or types.TYPE_TWO?

I'm trying

const { one, two } = yield race({
  one: take(types.TYPE_ONE),
  two: take(types.TYPE_TWO)
})

But two never seems to take unless I place it before one as follows (in which case one no longer takes):

const { one, two } = yield race({
  two: take(types.TYPE_TWO),
  one: take(types.TYPE_ONE)
})

docs need a complete rework

Actual documentation was mainly intended to showcase the library capabilities and how it is different from other middlewares. I think docs need to be reworked with a more 'conventional' approach

My initial list

1- API reference
2- Step by step tutorials
3- Section on unit testing Sagas (maybe also integration testing with store/middleware)
4- Examples on how to manage common concurrency patterns (watch and fork, watch and fork latest...)
5- Example solutions for some common flows (authorization, infinite scroll ...)
6- Links to external resources (good tutorials on Generators, redux-saga articles, app examples)

Docs will use Gitbook format

Any further ideas ?

Calling sagas from within components

Saga noob here. I was able to get quickly up and running using a bootstrapping/startup saga that pulls in async data the first time the app loads. I now want to be able to call sagas at will from within my React components just like I'd dispatch redux actions, after let's say a button press, or after moving to a new route (I'm using react-router). I can do that easily using redux because I have access to the dispatch through the connect(). What's the equivalent facility that redux-saga provides? My searches didn't get me anything, although I admit there's some new terminology here so I may be looking in the wrong places.

Isn't Saga just solving the same problem that custom middleware solves?

Currently my actions look like this

export function getFormula(id) {
  return {
    type: GET_FORMULA,
    [PARSE]: {
      parseClass: new FORMULA_CLASS(),
      method: 'GET',
    },
    [PROMISE]: {
      resolvedAction: {
        id
      },
      rejectedAction: {
        id
      }
    }
  }
}

The actions themselves have no functions in thunks or anything. Just instructions.

Likewise with Saga you'd just dispatch an action, but more plainly

export function getFormula(id) {
  return {
    type: GET_FORMULA,
    id,
  }
}

But inside the saga defined for it you'd still have to write out basically the same things that I wrote in both my action and in all my middleware for that action.

Also middleware can be decoupled and tested just like how sagas can be individually tested.

So aren't sagas just a different way of exposing the middleware API that redux has?

I ask this not to deprecate sagas (I think they're great) , but because I'm looking for a reason to switch haha.

Missing put / delay imports in README examples

In the README, put and delay are missing from import. Is this intentional? Being new to this library, I was immediately thrown off, and went searching where they came from..

import { take, call } from 'redux-saga'
// sagas/index.js
function* incrementAsync() {

  while(true) {

    // wait for each INCREMENT_ASYNC action  
    const nextAction = yield take(INCREMENT_ASYNC)

    // call delay : Number -> Promise
    yield delay(1000)

    // dispatch INCREMENT_COUNTER
    yield put( increment() )
  }

}

export default [incrementAsync]

Question: Canceling sagas from "upstream"

This is almost the inverse of this issue.

In summary, I have a project where the user can generate an entity called an audience. It can take several seconds for the audience to be created through a series of asynchronous steps, only one audience can be in view at a time, and they may create several audiences in series.

More concretely: the user has a large set of controls that they can interact with to generate a new audience. Any time they manipulate the controls, a new audience is requested from the server, but the controls remain "unlocked" for the user to further refine their input and generate subsequent audiences. I need to ensure that the last audience they requested is the one being shown, but I need to be able to generate the entities in parallel. Here's what I've come up with (somewhat simplified):

function* createAudienceSaga (getState) {
  let action, cancelLastAudienceBuild = () => {};

  // When a new audience create action comes along, cancel the previous one
  // by resolving the currently-active promise. We're using a promise here
  // so that `race(...)` calls can be our mechanism for determining cancelation
  /*eslint no-cond-assign:0 */
  while (action = yield take(CREATE_AUDIENCE)) {
    cancelLastAudienceBuild();

    let transaction = new Promise(r => cancelLastAudienceBuild = r);

    yield fork(buildAudience, transaction, action.payload);
  }
}

// Create a new audience
function* buildAudienceFromPlaceId(tx, params) {

  // Step 1: retrieve the place that the user wants to build the audience in
  const { place, canceled_0 } = yield race({
    place: call(api.places.get, params.placeId),
    canceled_0: tx
  });

  // If the user has fiddled with the UI, we are generating a new audience
  // in parallel and we can bail out of this one before doing any more work
  if(canceled_0) { return; }

  // Update the place information in the app state ASAP to show the user:
  yield put(showPlace(place));

  // Step 2: Create a new audience with the provided definition
  // This does not create the audience, but rather returns the audience ID,
  // which we have to request separately. The creation process is usually
  // very fast, but the step 3 can take several seconds.
  const { audienceId, canceled_1 } = yield race({
    audienceId: call(createAudienceWithDefinition, {
      definition: params.definition,
      geography: [makePlaceGeography(place)],
      product_id: params.product
    }),
    canceled_1: tx
  });

  // Again, check if a new transaction has begun before continuing:
  if(canceled_1) { return; }

  // Step 3: Retrieve the new audience data for display
  // As mentioned, this step can take several seconds for certain parameters.
  const { audience, canceled_2 } = yield race({
    audience: call(api.audience.get, audienceId),
    canceled_2: tx
  });

  // Last time, check to see if the user has changed the controls before
  // dispatching the new audience to the app state:
  if(canceled_2) { return; }

  yield put(showAudience(audience));
}

Yes, I know this is a pathological use case. I can't fix it, but I'm looking for a straightforward way to box this complexity up and not have to think about it.

It would be nice to have a feature on tasks that lets you cancel them from the outside. I don't know how that would work in practice without altering the API for tasks pretty significantly, or altering the internals of the saga state machine (which I haven't looked at closely and don't fully understand).

My other thought is that there's a simple and obvious way to do this that I'm not seeing.

es6 generators, promises, abort xhr?

Sorry for the plethora of questions lately @yelouafi; I find this idea very intriguing and want to start using it in a real world app, but there are quite a few unknowns I would like cleared up first.

I'm curious if it's possible to abort an outstanding xhr request that was fired via a promise, as a result of a generator invocation? I have poured over a ton of different blogs / specs talking about generators and their relation to promises, but I haven't found any instances of this behavior. Bluebird 3.0 offers a pipeline for cancellation, but I'm not sure if it's possible to both yield a promise (and use its value on success) and cancel that generator (and then cancel the promise)?

    function* doFetch(id) {
        let req
        try {
            // I want to be able to do something like this....
            yield req = xhr({
                method: 'GET'
                , url: `${api.fetchEndpoint}/${id}`
            })
        } catch (e) {
            // on cancel abort the xhr
            if (e instanceof io.SagaCancellationException) {
                req.cancel()
            }
        }
    }

    function* fetchThing(id) {
        try {
            const { body } = yield io.call(doFetch, id)
            // do stuff with the response body
        } catch (e) {
            // capture SagaCancellationException, and any js errors
            // resulting from an early exit of the yield call (such as body is not defined)
        }
    }

    // the root saga generator
    function* root() {
        let fetch
        while (true) {
            const { thing } = yield io.take(actions.SELECT_THING)

            // if user selects a different thing, cancel current fetch
            if (fetch && fetch.isRunning()) {
                yield io.cancel(fetch)
            }

            fetch = yield io.fork(fetchThing, thing.id)
        }
    }

What's the right way to go about this, assuming it's possible at all?

A yield req = xhr() continues immediately (as shown by a console.log(req) -> { the promise object }), so I get an error in the fetchThing catch of "cannot read property 'body' of undefined", but if cancelled the req object is defined in the error block. On the other hand, a req = yield xhr() waits for the result before continuing, and properly returns the xhr request body, but if cancelled req is not defined in the error block.

Testing error handling?

This just a question, not a real issue, but in your section on Error Handling you have the following example:

function* checkout(getState) {

  while( yield take(types.CHECKOUT_REQUEST) ) {
    try {
      const cart = getState().cart
      yield call(api.buyProducts, cart)
      yield put(actions.checkoutSuccess(cart))
    } catch(error) {
      yield put(actions.checkoutFailure(error))
    }
  }
}

How would I go about testing the error case? I'd like to test what happens when the function passed in the call statement rejects its Promise or throws an Error.

The best thing I can think of is trying to mock the call function to just throw an error, but that seems messier than I'd like. Any ideas? Thanks!

Code splitting / dynamically loading sagas

I just discovered redux-saga and I am looking to start integrating this into my app. I am currently code splitting my reducers and using replaceReducer when they hit the correct route. From the saga examples it looks like they all use a rootSaga. The closest I found to an answer is #23 (comment) but I am not certain if this is the official API and usage.

My current setup looks something like this: http://stackoverflow.com/a/33045558 which I pasted a snippet below:

function createRoutes(reducerRegistry) {
  return <Route path="/" component={App}>
    <Route path="async" getComponent={(location, cb) => {
      require.ensure([], require => {
        reducerRegistry.register({async: require('./reducers/async')})
        cb(null, require('./screens/Async'))
      })
    }}/>
  </Route>
}

function createStore(reducerRegistry) {
  var rootReducer = createReducer(reducerRegistry.getReducers())
  var store = createStore(rootReducer)

  reducerRegistry.setChangeListener((reducers) => {
    store.replaceReducer(createReducer(reducers))
  })

  return store
}

From the other comment, it sounds like this should be possible, but it looks like sagas need access to the store?

    <Route path="async" getComponent={(location, cb) => {
      require.ensure([], require => {
        reducerRegistry.register({async: require('./reducers/async').default})
        const sagas = require('./sagas').default
        // store?
        runSaga(sagas) 
        // right api usage? where to get the store? does any array work?
        sagas.forEach(saga => runSaga( saga(store.getState), store )) 
        // only supports one rootSaga, not an array?
        runSaga( sagas(store.getState), store )
        cb(null, require('./screens/Async'))
      })
    }}/>

It looks like a middleware approach was discussed in #13 (comment) that would have solved this, but it was closed. This could have potentially worked:

  var store = createStore(rootReducer)

  reducerRegistry.setChangeListener(reducers => {
    store.replaceReducer(createReducer(reducers))
  })

  sagaRegistry.setChangeListener(iterator => {
    store.runSaga(iterator)
  })

Would this be the official usage? What about "stopping/removing/disconnecting" the sagas? Will this work with server side rendering?

Watching non-blockingly for actions

Consider in some Component:

handleLocalFileUpload(files) {
  files.forEach(file => {
    console.log(file);
    this.props.createAssetFromLocal(file);
  })
}

with an action creator

export const createAssetFromLocal = file => ({
  type: CREATE_ASSET_FROM_LOCAL_REQUEST,
  file
});

and the corresponding watcher

export const watchCreateAssetFromLocal = function *(getState) {
  while (true) {
    const { file } = yield take(actions.CREATE_ASSET_FROM_LOCAL_REQUEST);
    yield fork(createAssetFromLocal, file, getState);

    console.log('done');
  }
};

When calling handleLocalFileUpload(['file 1', 'file 2']), we have the problem that two actions of type CREATE_ASSET_FROM_LOCAL_REQUEST will be dispatched shortly after, and the watcher misses the second one. The corresponding log:

file 1
file 2
done

Is there something we are doing wrong? I.e. is there a way to write a watcher that isn't blocked for a short moment?

Decouple from Redux

Hi,

I think this project could be useful outside of Redux. As far as I know, there is no Redux dependency and little coupling.

I would be interested in using this project outside of Redux.

The coupling points seems to be:

  • The put/take effects
  • The middleware

You could build a declarative effect programming lib in a separate project, and provide entry-points for put/take effects as plugin effects.

Then Redux-saga would give us a middleware and a meaningful set of default plugins for take/put on top of the original lib, and I could make my own integration for my own framework too.

(I think the name "Saga" does not really make any sense anymore if it does not deal with event effects btw)

Sagas should rather be totally autonomous

Hello,

I've seen the real world where some sagas need to be stateful to know if the data needs to be fetched or not:

export default function* root(getState) {

  const getUser = login => getState().entities.users[login]
  const getRepo = fullName => getState().entities.repos[fullName]
  const getStarredByUser = login => getState().pagination.starredByUser[login]
  const getStargazersByRepo = fullName => getState().pagination.stargazersByRepo[fullName]

  yield fork(watchNavigate)
  yield fork(watchLoadUserPage, getUser, getStarredByUser)
  yield fork(watchLoadRepoPage, getRepo, getStargazersByRepo)
  yield fork(watchLoadMoreStarred, getStarredByUser)
  yield fork(watchLoadMoreStargazers, getStargazersByRepo)
}

// Fetches data for a User : user data + starred repos
function* watchLoadUserPage(getUser, getStarredByUser) {
  while(true) {
    const {login, requiredFields = []} = yield take(actions.LOAD_USER_PAGE)

    yield fork(loadUser, login, getUser(login), requiredFields)
    yield fork(loadStarred, login, getStarredByUser(login))
  }
}

// load user unless it is cached
function* loadUser(login, user, requiredFields) {
  if (!user || requiredFields.some(key => !user.hasOwnProperty(key))) {
    yield call(fetchUser, login)
  }
}

// load next page of repos starred by this user unless it is cached
function* loadStarred(login, starredByUser = {}, loadMore) {
  if (!starredByUser.pageCount || loadMore)
    yield call(
      fetchStarred,
      login,
      starredByUser.nextPageUrl || firstPageStarredUrl(login)
    )
}

I think we already discussed that but I think the Saga should be a totally autonomous process that listen for events and perform effects.

The problem here for me is that getState().entities.users[login] is actually a state that has the purpose of being displayed to the UI, as it is computed by Redux reducers. So basically you are coupling the way a Saga may perform effects to the UI state. Your saga is not really stateful, but it can use state provided by a dependency (the UI state).

I think the Saga should not know anything about the UI state at all. Refactoring the layout of the UI state should not need to perform any modification to the saga logic.

In backend systems, sagas can be distributed across a cluster of machines, and the saga can't really (or efficiently) query synchronously the state of the app as it may be stored on other machines. That's why Sagas are stateful and decoupled on the backend.

Maybe we should not force the user to use this decoupling as it introduces more complexity, but at least give the opportunity for the Saga to really be stateful, instead of reusing the UI state provided by getState. A simple possibility would be to register a reducer to the Saga for example.

See for example a saga implemented in Java here: http://www.axonframework.org/docs/2.0/sagas.html

public class OrderManagementSaga extends AbstractAnnotatedSaga {

    private boolean paid = false;
    private boolean delivered = false;
    private transient CommandGateway commandGateway;

    @StartSaga
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(OrderCreatedEvent event) {
        // client generated identifiers (1)
        ShippingId shipmentId = createShipmentId();
        InvoiceId invoiceId = createInvoiceId();
        // associate the Saga with these values, before sending the commands (2)
        associateWith("shipmentId", shipmentId);
        associateWith("invoiceId", invoiceId);
        // send the commands
        commandGateway.send(new PrepareShippingCommand(...));
        commandGateway.send(new CreateInvoiceCommand(...));
    }

    @SagaEventHandler(associationProperty = "shipmentId")
    public void handle(ShippingArrivedEvent event) {
        delivered = true;
        if (paid) {
            end(); (3)
        }
    }

    @SagaEventHandler(associationProperty = "invoiceId")
    public void handle(InvoicePaidEvent event) {
        paid = true;
        if (delivered) {
            end(); (4)
        }
    }

    // ...

}

As you can see, the OrderManagementSaga is created after every OrderCreatedEvent (so many OrderManagementSaga can live at the same time in the system, but this probably does not apply to frontend sagas). These sagas are stateful and have the paid and delivered attributes.

This is just the Saga code, but you can guess that there's in the system another item called Shippement that stores an attribute delivred.

This may seem surprising but it is not a problem if the global system stores the same data in multiple places. Each place can pick the data it needs from the events. This permits to avoid introducing new dependencies. The only real shared dependency all the components have is the event log.

The current approach of using Redux' getState() in Sagas for me is a bit similar to using waitFor of Flux. It works but creates coupling that can be avoided.

Components exporting sagas

I think it'd be neat if you could use sagas for things like animation, or other things that might be unique to particular components. Is there a suggested pattern for this? E.g. exporting a sagas array or something.

Sagas started with runSaga can't take actions from Saga started with the middleware

Let's say I have 2 sagas:

  1. simple api saga that was provided to middleware:
function* apiSaga(getState){
   while(true){
      const action  = yield take('API')
      const { result, error } = yield call(requestApiService, action)
      const [reqType, successType, failType ] = action.types

      yield put({ type: reqType, action })
      if(error) {
         yield put({ type: failType, error })
      } else {
         yield put({ type: successType, result})
      }
   }
}
  1. another saga, that I want to call with runSaga method:
function* routeResolveSaga(getState){
   yield put({ type: 'API', ..... types: ['ACTION_REQUEST', 'ACTION_SUCCESS', 'ACTION_FAIL'] })
   yield take(['ACTION_SUCCESS', 'ACTION_FAIL'])
}

When I call

runSaga(function*(getState){ 
  yield call(routeResolveSaga, getState) 
}(store.getState), storeIO(store))

apiSaga takes API action, after request is puts new action ACTION_SUCCESS or ACTION_FAIL, routeResolveSaga is waiting to take it, but nothing happens.

Maybe I have missed something?

Adding support for calling function with `this` context (object/class methods)

Consider the class

export default class API {
  fetch() {
    console.log(this);
  }
}

With backend = new API(), the call yield call(backend.fetch) logs undefined, whereas yield backend.fetch() logs the correct this.

Edit: yield call(backend.fetch.bind(backend)) works fine, too. So feel free to close this if you don't consider this an bug.

Create a test-utils library with raceResult()

I've recently been writing some tests for a race condition and Im really impressed with how easy it is to test. There's one thing that bothers me a little bit which is how the tester needs to know a bit about the implementation of race.

Lets say we have the following (inside some loop with an interval variable):

 const { stop, update } = yield race({
    delay: call(delay, interval),
    update: take('UPDATE'),
    stop: take('STOP'),
  })

  if (stop) {
    break;
  } else if (update) {
    interval = update.payload.interval;
  }

I can do a test like:

   let next = generator.next()
    expect(next.value).to.deep.equal(race({
      delay: call(delay, 100),
      update: take('UPDATE'),
      stop: take('STOP'),
    }))

    // Assume update wins
    next = generator.next({ update: { payload: { interval: 50 } }, start: null, delay: null })
    // Test something on the update

This is actually pretty good, but it seems like I have to know something about the internals of race. I wonder if it might make more sense to include a raceResult testUtility that allows you to abstract this knowledge away and if in the future the internals of race change, it would be transparent.

Id imagine something like:

generate.next(raceResult({
   update: { payload: { interval: 100 }
})

or even generate.next(raceResult('update', { payload: { interval: 100 })) and have it handle the unpacking to null.

Do you think this would be something useful? If so I could swing a PR together. I could envision several testing utilities for the spec mode, maybe there is one for handling the tasks which have been popping up in other issues.

Using Sagas like middleware to reduce boilerplate, and putting actions to only other Sagas

Hey so I was following along with your real world example, but there was a whole bunch of repeated logic:

For example in your actions file:

export const USER = createRequestTypes('USER')
export const REPO = createRequestTypes('REPO')
export const STARRED = createRequestTypes('STARRED')
export const STARGAZERS = createRequestTypes('STARGAZERS')

// ....other stuff

export const user = {
  request: login => action(USER.REQUEST, {login}),
  success: (login, response) => action(USER.SUCCESS, {login, response}),
  failure: (login, error) => action(USER.FAILURE, {login, error}),
}

export const repo = {
  request: fullName => action(REPO.REQUEST, {fullName}),
  success: (fullName, response) => action(REPO.SUCCESS, {fullName, response}),
  failure: (fullName, error) => action(REPO.FAILURE, {fullName, error}),
}

export const starred = {
  request: login => action(STARRED.REQUEST, {login}),
  success: (login, response) => action(STARRED.SUCCESS, {login, response}),
  failure: (login, error) => action(STARRED.FAILURE, {login, error}),
}

export const stargazers = {
  request: fullName => action(STARGAZERS.REQUEST, {fullName}),
  success: (fullName, response) => action(STARGAZERS.SUCCESS, {fullName, response}),
  failure: (fullName, error) => action(STARGAZERS.FAILURE, {fullName, error}),
}

and then in your actual sagas file

const fetchUser       = fetchEntity.bind(null, user, api.fetchUser)
const fetchRepo       = fetchEntity.bind(null, repo, api.fetchRepo)
const fetchStarred    = fetchEntity.bind(null, starred, api.fetchStarred)
const fetchStargazers = fetchEntity.bind(null, stargazers, api.fetchStargazers)

There's a lot more too, but I think it would be redundant to post it.

I used that as a guide for my own personal project since I had something really similar, but I realized that I could cut down on a lot of that by writing an apiCall saga and a promise Saga much like how one would have written middleware:

// Create lifecycle of a promise
const createPromiseLifecycle = (base) => ({
  PENDING:  `${base}_PENDING`,
  RESOLVED: `${base}_RESOLVED`,
  REJECTED: `${base}_REJECTED`
})

function* apiCall() {
  while(true) {
    // Grab all actions with the API intention
    const { type, payload: { method, options } } = yield take((action) => action[actions.API])

    // Pass on the pending API call to the promise saga
    yield put({
      [actions.PROMISE]: true,
      type,
      payload: {
        promise: call(api[method], options)
      }
    })
  }
}

function* promise() {
  while(true) {
    // Grab all actions with the PROMISE intention
    const action = yield take((action) => action[actions.PROMISE])
    const lifecycle = createPromiseLifecycle(action.type)

    // Put out the fact the promise is pending
    yield put({ type: lifecycle.PENDING })

    // Try to resolve the promise
    try {
      const result = yield action.payload.promise
      yield put ({
        type: lifecycle.RESOLVED,
        payload: {
          result
        }
      })
    } catch(err) {
      yield put ({
        type: lifecycle.REJECTED,
        payload: {
          err
        }
      })
    }
  }
}

I have two questions about this:

  1. Is this against what sagas are about? I've just interpreted them as middleware with much more advanced capabilities.
  2. Is there any equivalent to next from middleware in sagas? If you noticed I do a put in my apiCall, but it has an undesired behavior as it also sends that action to the reducers. I want to be able to do a put that only my sagas can receive basically.

Any idea to test this generator function?

export function * getData () {
  let task
  while (true) {
    let { data } = yield take(types.GET_DATA)
    task && task.isRunning() ? yield cancel(task) : null
    task = yield fork(callApi, data)
  }
}

race function not behaving as expected.

I have a bit of a strange situation going on, perhaps as a result of incorrect usage.

I am attempting to construct a saga that involves a race between two actions types.

  const { explicitLogout, dashboardChange } = yield race({                                                                                                                                                                                                                                          
      dashboardChange: take(DASHBOARD_LOAD_REQUEST),                                                                                                                                                                                                                            
      explicitLogout: take(LOGOUT),                                                                                                                                                                                                                                            
    }) 

For whatever reason, if I trigger a LOGOUT action, the race doesn't seem to resolve and produce a winner. However, if I trigger a DASHBOARD_LOAD_REQUEST, it does.

Making matters slightly more confusing is the fact that if a limit the race to:

 const { explicitLogout } = yield race({                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
      explicitLogout: take(LOGOUT),                                                                                                                                                                                                                                            
    }) 

I am able to resolve the race by triggering a LOGOUT action.

Is there something I am missing here? Is it not possible to compose a race of multiple "take" effects?

Catching misspelled patterns for take

From the docs, take() or take('*') may be used for matching all actions, however if an undefined value is passed explicitly, it's typically an indicator of a misspelled constant. What do you think about either only using take('*') for matching all actions, adding a check that would error in cases which are likely user error:

export function take(pattern){
  if (arguments.length > 0 && is.undef(pattern)) {
    throw new Error(INVALID_PATTERN)
  }
  return effect(TAKE, is.undef(pattern) ? '*' : pattern)
}


take(constant.MISSPELLED_PATTERN) // would throw

cancellation too presumptive?

Just some food for thought...

Nevertheless, a warning is logged into the console in case a cancelled task omitted to handle a cancellation exception.

I have quite a few instances where one saga spawns a fetcher function, and that fetcher func then invokes the actual function doing the xhr. This is to keep functions lean and "doing one thing". Maybe I want to transform the parameters or data before/after fetching, or maybe I want to check status codes and validate returned values versus nulls, etc.

    function doFetch(params) {
        yield xhr(url, params)
    }

    function* fetch(params) {
        const { statusCode, body } = yield call(doFetch, params)

        if (statusCode > 499) {
            return null
        }

        const data = body.someData

        // ... do some transformations on the data

        return {
            transformedData
        }
    }

    function* fetchSomeStuff() {
        yield put(START_FETCH)

        // ...maybe configure some parameters

        const results = yield call(fetch, params)

        // ...do some data transforms on results

        yield put(FETCH_SUCCESS, results)
    }

When I want to sure up this code with some error handling, in order to avoid a console.warn by redux-saga I have to put in a try/catch for every step along the saga. This is fairly inconvenient to have to handle a thrown saga cancelled exception at every invocation, particularly because I only want to use the catch to "cleanup" in one of the cases:

    function doFetch(params) {
        let req
        try {
            return (yield req = xhr(url, params))
        } catch (e) {
            if (e instanceof SagaCancellationException) {
                req.abort()
            } else {
                throw e
            }
        }
    }

    function* fetch(params) {
        // doesn't have to handle errors, since there's nothing to do and the
        // caller handles anything that might be thrown... 
        try {
            // ... same code as above
        } catch (e) {
            // there's nothing to do here except stop the console.warn() from showing up
        }
    }

    function* fetchSomeStuff() {
        try {
            yield put(START_FETCH)

            // ...maybe configure some parameters

            const results = yield call(fetch, params)

            // ...do some data transforms on results

            yield put(FETCH_SUCCESS, results)
        } catch (e) {
            // capture SagaCancellationException, but throw any js errors
            if (!e instanceof io.SagaCancellationException) {
                console.error(e)
            }
        }
    }

Handling saga IO from within callbacks

Yielding a call to a function that is using a callback to run its post execution behavior: yield apply(someObject, someFunction)

Seems I can't yield put from someFunction since the callback isn't a generator. How can I dispatch an action from that callback?

Symbol() not a thing on Android/iOS < 9 in React Native

When using redux-saga in React Native on the Android platform, the Javascript environment doesn't seem to support Symbols.

I fired up an older version (0.3.3) of redux-saga and replaced all the Symbol() calls with strings and everything worked great.

In the later codebases (0.5.0), Symbols are used much more for iterators and stuff.

Was wondering if you'd had any thoughts about perhaps supporting execution environments without Symbol() support.

I haven't dug into why it isn't available, but I can if you need more context.

Thanks! :)

Sagas not starting up in time for early actions

I was trying to figure out why my saga just continued to block on an action and it turned out that the action was dispatched too early (it appears before the first EFFECT_TRIGGERED, having been automatically dispatched from the componentWillMount of my first route's component). How can I assure that my sagas are up and running before any other actions are dispatched?

Testing problem

Hello !

Considering this code :

function* checkout(getState) {

  while( yield take(types.CHECKOUT_REQUEST) ) {
      const cart = getState().cart
      const { response, error } = yield call(api.buyProducts, cart)

     if (response)
      yield put(actions.checkoutSuccess(cart))
    } else {
      yield put(actions.checkoutFailure(error))
    }
  }
}

How would one test this code ?

At the moment, I'm looping over this code twice. Simulating an API success, then an API failure, to get both paths covered. But I get my first assertions twice too.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.