Coder Social home page Coder Social logo

diegohaz / redux-saga-thunk Goto Github PK

View Code? Open in Web Editor NEW
221.0 9.0 17.0 2.15 MB

Dispatching an action handled by redux-saga returns promise

License: MIT License

JavaScript 100.00%
redux redux-saga redux-middleware redux-reducers selectors redux-thunk flux-standard-action

redux-saga-thunk's Introduction

redux-saga-thunk

Generated with nod NPM version NPM downloads Build Status Coverage Status

Dispatching an action handled by redux-saga returns promise. It looks like redux-thunk, but with pure action creators.

class MyComponent extends React.Component {
  componentWillMount() {
    // `doSomething` dispatches an action which is handled by some saga
    this.props.doSomething().then((detail) => {
      console.log('Yaay!', detail)
    }).catch((error) => {
      console.log('Oops!', error)
    })
  }
}

redux-saga-thunk uses Flux Standard Action to determine action's payload, error etc.



If you find this useful, please don't forget to star ⭐️ the repo, as this will help to promote the project.
Follow me on Twitter and GitHub to keep updated about this project and others.



Motivation

There are two reasons I created this library: Server Side Rendering and redux-form.

When using redux-saga on server, you will need to know when your actions have been finished so you can send the response to the client. There are several ways to handle that case, and redux-saga-thunk approach is the one I like most. See an example.

With redux-form, you need to return a promise from dispatch inside your submit handler so it will know when the submission is complete. See an example

Finally, that's a nice way to migrate your codebase from redux-thunk to redux-saga, since you will not need to change how you dispatch your actions, they will still return promises.

Install

$ npm install --save redux-saga-thunk

Basic setup

Add middleware to your redux configuration (before redux-saga middleware):

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { middleware as thunkMiddleware } from 'redux-saga-thunk'
^

const sagaMiddleware = createSagaMiddleware()
const store = createStore({}, applyMiddleware(thunkMiddleware, sagaMiddleware))
                                              ^

Usage

You just need to set meta.thunk to true on your request actions and put it on your response actions inside the saga:

const action = {
  type: 'RESOURCE_REQUEST',
  payload: { id: 'foo' },
  meta: {
    thunk: true
    ^
  }
}

// send the action
store.dispatch(action).then((detail) => {
  // payload == detail
  console.log('Yaay!', detail)
}).catch((e) => {
  // payload == e
  console.log('Oops!', e)
})

function* saga() {
  while(true) {
    const { payload, meta } = yield take('RESOURCE_REQUEST') 
                     ^
    try {
      const detail = yield call(callApi, payload) // payload == { id: 'foo' }
      yield put({
        type: 'RESOURCE_SUCCESS',
        payload: detail,
        meta
        ^
      })
    } catch (e) {
      yield put({
        type: 'RESOURCE_FAILURE',
        payload: e,
        error: true,
        ^
        meta
        ^
      })
    }
  }
}

redux-saga-thunk will automatically transform your request action and inject a key into it.

You can also use it inside sagas with put.resolve:

function *someSaga() {
  try {
    const detail = yield put.resolve(action)
    console.log('Yaay!', detail)
  } catch (error) {
    console.log('Oops!', error)
  }
}

Usage with selectors

To use pending, rejected, fulfilled and done selectors, you'll need to add the thunkReducer to your store:

import { combineReducers } from 'redux'
import { reducer as thunkReducer } from 'redux-saga-thunk'

const reducer = combineReducers({
  thunk: thunkReducer,
  // your reducers...
})

Now you can use selectors on your containers:

import { pending, rejected, fulfilled, done } from 'redux-saga-thunk'

const mapStateToProps = state => ({
  loading: pending(state, 'RESOURCE_CREATE_REQUEST'),
  error: rejected(state, 'RESOURCE_CREATE_REQUEST'),
  success: fulfilled(state, 'RESOURCE_CREATE_REQUEST'),
  done: done(state, 'RESOURCE_CREATE_REQUEST'),
})

API

Table of Contents

clean

Clean state

Parameters

Examples

const mapDispatchToProps = (dispatch, ownProps) => ({
  cleanFetchUserStateForAllIds: () => dispatch(clean('FETCH_USER')),
  cleanFetchUserStateForSpecifiedId: () => dispatch(clean('FETCH_USER', ownProps.id)),
  cleanFetchUsersState: () => dispatch(clean('FETCH_USERS')),
})

pending

Tells if an action is pending

Parameters

Examples

const mapStateToProps = state => ({
  fooIsPending: pending(state, 'FOO'),
  barForId42IsPending: pending(state, 'BAR', 42),
  barForAnyIdIsPending: pending(state, 'BAR'),
  fooOrBazIsPending: pending(state, ['FOO', 'BAZ']),
  fooOrBarForId42IsPending: pending(state, ['FOO', ['BAR', 42]]),
  anythingIsPending: pending(state)
})

Returns boolean

rejected

Tells if an action was rejected

Parameters

Examples

const mapStateToProps = state => ({
  fooWasRejected: rejected(state, 'FOO'),
  barForId42WasRejected: rejected(state, 'BAR', 42),
  barForAnyIdWasRejected: rejected(state, 'BAR'),
  fooOrBazWasRejected: rejected(state, ['FOO', 'BAZ']),
  fooOrBarForId42WasRejected: rejected(state, ['FOO', ['BAR', 42]]),
  anythingWasRejected: rejected(state)
})

Returns boolean

fulfilled

Tells if an action is fulfilled

Parameters

Examples

const mapStateToProps = state => ({
  fooIsFulfilled: fulfilled(state, 'FOO'),
  barForId42IsFulfilled: fulfilled(state, 'BAR', 42),
  barForAnyIdIsFulfilled: fulfilled(state, 'BAR'),
  fooOrBazIsFulfilled: fulfilled(state, ['FOO', 'BAZ']),
  fooOrBarForId42IsFulfilled: fulfilled(state, ['FOO', ['BAR', 42]]),
  anythingIsFulfilled: fulfilled(state)
})

Returns boolean

done

Tells if an action is done

Parameters

Examples

const mapStateToProps = state => ({
  fooIsDone: done(state, 'FOO'),
  barForId42IsDone: done(state, 'BAR', 42),
  barForAnyIdIsDone: done(state, 'BAR'),
  fooOrBazIsDone: done(state, ['FOO', 'BAZ']),
  fooOrBarForId42IsDone: done(state, ['FOO', ['BAR', 42]]),
  anythingIsDone: done(state)
})

Returns boolean

License

MIT © Diego Haz

redux-saga-thunk's People

Contributors

bartekus avatar braska avatar dependabot[bot] avatar diegohaz avatar greenkeeper[bot] avatar raddeus avatar santino avatar tauka 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

redux-saga-thunk's Issues

Header 200, empty body = Unexpected end of JSON input

middleware.js:22 Uncaught (in promise) SyntaxError: Unexpected end of JSON input

Even though the request passed and received a 200 header code. The body is empty, though, and this makes an error in redux-saga-thunk

Payload ignored when error

In case of a failing request redux-saga-thunk is not rejecting with the payload passed in but just with the error property.
This should be fixed as the error property is just a boolean and we need to deal with responses to build client side behaviours in response of a failure.

TypeError: In this environment the sources for assign MUST be an object. This error is a performance optimization and not spec compliant.

I recently upgraded to the new version of redux-saga-thunk, but I'm now getting the following error when attempting to use it:

"TypeError: In this environment the sources for assign MUST be an object. This error is a performance optimization and not spec compliant."

This error appears as soon as I dispatch an action that has meta: { thunk: true }. I do not get the error when using redux-saga-thunk v0.6.1, but I do get it when upgrading to v0.7.0 or v0.7.1. I assume the error is coming from the code in 68d02a3 which introduces the use of Object.assign.

Not sure exactly what causes this error, but when using v0.6.1 or earlier I'm able to use redux-saga-thunk with no issues at all. I'm guessing that something about how Object.assign works in the React Native environment is conflicting with how Object.assign is being used in this library.

Environment:

{
    "react": "16.4.1",
    "react-native": "0.56.0",
    "redux": "4.0.0"
}

Can't pass `thunk` to `_extend`

Hey there:

Apologies if this is way out of scope for your module. My environment is pretty much:

react-native-cli: 2.0.1
react-native: 0.55.1
ignite: 2.1.0 (w/ ignite-ir-boilerplate-andross)
redux-saga: ^0.16.0
redux-saga-thunk: 0.7.1
reduxsauce: 0.7.0
mac os: 10.13.5

Maybe I'm doing something wrong here, or maybe no one else has tried, but not sure how/if I'm the only one who has faced this issue..

I've got a component with the following method (set up this way since the component is one of many in a list, and want to keep track of requests per item in list):

  async onPress () {
    this.setState({
      fetching: true
    })
    await this.props.deleteTodosRequest(this.props.todo)
    this.setState({
      fetching: false
    })
  }

And the basic map helper:

const mapDispatchToProps = (dispatch) => {
  return {
    deleteTodosRequest: (todo) => dispatch(DeleteTodosActions.deleteTodosRequest(todo))
  }
}

I've written my action creators like so using reduxsauce:

const { Types, Creators } = createActions({
  deleteTodosRequest: (data) => ({
    type: 'DELETE_TODOS_REQUEST',
    data,
    meta: {
      thunk: true
    }
  }),
  deleteTodosSuccess: ['payload'],
  deleteTodosFailure: null
})

And my saga:

export function * deleteTodos (api, action) {
  const { data } = action
  // get current data from Store
  // const currentData = yield select(DeleteTodosSelectors.getData)
  // make the call to the api
  const response = yield call(api.deleteTodos, data)

  // success?
  if (response.ok) {
    // You might need to change the response here - do this with a 'transform',
    // located in ../Transforms/. Otherwise, just pass the data back from the api.
    yield put(DeleteTodosActions.deleteTodosSuccess(response.data))
    yield put(TodosActions.removeTodos(data))
  } else {
    yield put(DeleteTodosActions.deleteTodosFailure())
  }
}

What I'm seeing here is that the following method from utils.js on line 66:

var generateThunk = exports.generateThunk = function generateThunk(action) {
  var thunk = getThunkMeta(action);

  return hasKey(action) ? _extends({}, thunk, {
    type: 'RESPONSE'
  }) : _extends({}, thunk || {}, {
    name: getThunkName(action),
    key: Math.random().toFixed(16).substring(2),
    type: 'REQUEST'
  });
};

tries to pass a truthy value to the _extend() method which throws the following error:

TypeError: In this environment the sources for assign MUST be an object.

I was able to solve this one of three ways. One by returning just the meta object in the getThunkMeta() method:

var getThunkMeta = exports.getThunkMeta = function getThunkMeta(action) {
  if (isThunkAction(action)) {
    return action.meta;
  }
  return null;
};

Two by wrapping the thunk in an object:

var generateThunk = exports.generateThunk = function generateThunk(action) {
  var thunk = getThunkMeta(action);

  return hasKey(action) ? _extends({}, { thunk: thunk }, {
    type: 'RESPONSE'
  }) : _extends({}, { thunk: thunk } || {}, {
    name: getThunkName(action),
    key: Math.random().toFixed(16).substring(2),
    type: 'REQUEST'
  });
};

Or three by adding an {} instead of a true to the action creator meta:

const { Types, Creators } = createActions({
  deleteTodosRequest: (data) => ({
    type: 'DELETE_TODOS_REQUEST',
    data,
    meta: {
      thunk: {}
    }
  }),
  deleteTodosSuccess: ['payload'],
  deleteTodosFailure: null
})

This wasn't tested against anything else tho, so not sure of implications.. If feasible I'm happy to submit a PR.

ACTION_A should be dispatched before ACTION_B

First of all, great library, it's been really useful in allowing my components that dispatch an action to perform some additional functionality based on the eventual response from redux-saga.

I'm getting this error thrown from the redux-saga-thunk middleware and I think my usage of redux-saga-thunk is normal/valid and shouldn't result in any errors. The error is being thrown at https://github.com/diegohaz/redux-saga-thunk/blob/master/src/middleware.js#L26.

I have a standard action being dispatched, ACTION_A, with meta: { thunk: true }. I have a standard redux store configuration (matches the redux-saga-thunk "Basic setup" instructions) and a standard redux-saga setup, with my sagas essentially looking like this:

// sagas.js

function* handleActionA(action) {
  yield put({ type: 'ACTION_B', meta: action.meta });
}

function* watchActionA() {
  yield takeEvery('ACTION_A', handleActionA);
}

function* rootSaga() {
  yield fork(watchActionA);
}

export default rootSaga;

This is obviously simplified, but this is essentially what I'm doing. In my case, if handleActionA determines that some value has already been fetched from the API and is available in the redux store, then it just dispatches ACTION_B. So the result of this is that ACTION_A is dispatched from a component in my app, and this is picked up by the watchActionA saga, and then in handleActionA I immediately dispatch ACTION_B.

redux-saga-thunk then throws the error "[redux-saga-thunk] ACTION_A should be dispatched before ACTION_B". But this doesn't make sense, because ACTION_A was dispatched before ACTION_B, and with my saga setup, the only way that ACTION_B can be dispatched is after a saga take's an ACTION_A and runs another function which then dispatches ACTION_B, so it's actually impossible for ACTION_B to be dispatched before ACTION_A (which the current wording of the error message suggests is happening).

This seems to be some sort of race condition that occurs when the success/failure action is dispatched too quickly after the initial request action? I was able to avoid this error being thrown by adding yield delay(100); right before yield put({ type: 'ACTION_B', meta: action.meta }); in handleActionA but that is obviously not a good solution.

Add success selector

Please consider adding a isDone or isSuccess selector next to hasFailed and isPending

export const PENDING = 'pending'
export const FAILURE = 'failure'

Redux 4 ?

Howdy @diegohaz, thank you for all your work on redux-saga-thunk as well as ARc project, you rock good sir!

Now, regarding the plans to upgrade to Redux 4, is there a timeframe for this to happen, or nothing concrete yet?

Thx

Ability to add identifier (optional) to thunk

Case:

Sometimes we use same action type to load different entities.

For example: you can dispatch LOAD_MY_ITEM to start fetching data from API. And you pass itemId to action payload.

In this case we have ability to use isPending selector in order to show spinner in UI. But if we load 2 items in the same time we can't distinguish which item loading currently in progress.

I suggest to introduce identifier that can be passed to sub-field of meta object of action. In this case we can check thunk state with using isPending selector in following way: isPending(state, LOAD_MY_ITEM, ownProps.itemId).

Ability to invalidate action state trough dispatching exported action

What do you thing about possibility to implement exportable action (for example invalidate) that can be dispatched from user space to invalidate state of specific action?

For example:

import React from 'react';
import { invalidate, fulfilled } from 'redux-saga-thunk';
import { connect } from 'react-redux';

const MyComponent = ({ fulfilled, invalidate }) => (
  <div>
    <div>Is fulfilled: {fulfilled ? 'true' : 'false'}</div>
    {fulfilled && <button onClick={invalidate}>Invalidate</button>}
  </div>
);

export default connect(state => ({
  fulfilled: fulfilled(state, 'MY_CUSTOM_ACTION'),
}), { invalidate })(MyComponent); 

Clicking on button will trigger setting fulfilled to false.

Typescript declarations

This package needs typescript declarations to pass ts compiler.

Since it has only one exported member, index.d.ts is going to contain just one line

export const middleware: any;

As per docs, no other API is exposed

I'd like to open a PR, if you consider this necessary

An in-range update of eslint-plugin-import is breaking the build 🚨

Version 2.4.0 of eslint-plugin-import just got published.

Branch Build failing 🚨
Dependency eslint-plugin-import
Current Version 2.3.0
Type devDependency

This version is covered by your current version range and after updating it in your project the build failed.

As eslint-plugin-import is “only” a devDependency of this project it might not break production or downstream projects, but “only” your build or test tools – preventing new deploys or publishes.

I recommend you give this issue a high priority. I’m sure you can resolve this 💪

Status Details
  • continuous-integration/travis-ci/push The Travis CI build could not complete due to an error Details

Commits

The new version differs by 10 commits.

  • 44ca158 update utils changelog
  • a3728d7 bump eslint-module-utils to v2.1.0
  • 3e29169 bump v2.4.0
  • ea9c92c Merge pull request #737 from kevin940726/master
  • 8f9b403 fix typos, enforce type of array of strings in allow option
  • 95315e0 update CHANGELOG.md
  • 28e1623 eslint-module-utils: filePath in parserOptions (#840)
  • 2f690b4 update CI to build on Node 6+7 (#846)
  • 7d41745 write doc, add two more tests
  • dedfb11 add allow glob for rule no-unassigned-import, fix #671

See the full diff

Not sure how things should work exactly?

There is a collection of frequently asked questions and of course you may always ask my humans.


Your Greenkeeper Bot 🌴

Change selectors names

Per @Geczy's suggestion on #16 (comment), I agree with renaming selectors. I just think the current names should be kept as aliases (something like export const hasFailed = failed).

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.