Promises in redux-saga
Asked Answered
Z

4

12

I found the same question here, but without a proper answer I am looking for.

I am developing a simple application with CRUD operations. On the edit page, after the component gets mounted (componentDidMount()), the app dispatches an action to retrieve a specific post details:

dispatch({ type: FETCH_POST, id: 'post-id' })

I am using redux-saga and want the above call to return a Promise so that I can access the API response.

Right now, without a callback/Promise, I ended up with defining a new state in store (like post_edited) and connect/map it to props in the component for edit page.

What would be the best possible way to deal with this kind of situation?

Zadoc answered 17/2, 2017 at 21:25 Comment(0)
B
22

Could you please provide more information about your issue? I'm not sure if I understand your issue properly, but the common practice is:

API.js

function apiCallToFetchPost(id) {
  return Promise.resolve({name: 'Test});
}

postSaga.js

function* fetchPostSaga({id}) {
  try {
    const request = yield call(apiCallToFetchPost, id);
    // -> in post reducer we will save the fetched data for showing them later 
    yield put({type: FETCH_POST_SUCCESS, payload: request}); 
  } catch (error) {
    yield put({type: FETCH_POST_SUCCESS_FAILURE, error})
  }
}

export function* onBootstrap() {
  yield takeLatest(FETCH_POST, fetchPostSaga);
}
Bayles answered 17/2, 2017 at 23:41 Comment(2)
There is already a state variable to keep the list of posts. Then do we need to have two states, one for the list posts, and the other one for a single resource to edit/view post? Is is the common/best practice? Actually I didn't want to add another state for a single post.Zadoc
This code isn't doing anything with state, it's just causing actions to be dispatched. Your reducer code decides what to do with the fetched data; there's no reducer code here. If you already have other code that fetches the entire list of posts, you wouldn't need any of this. You can just create a selector function in your reducer (like getPostById(state, id)) and call it in mapStateToProps().Bronchus
S
4

There's a package that does exactly what the OP requested, i.e. arranges that dispatch() can return a promise: @adobe/redux-saga-promise Using it, you define a "promise action" creator via:

import { createPromiseAction } from '@adobe/redux-saga-promise'

export const fetchPostAction = createPromiseAction('FETCH_POST')

The dispatch() of a "promise action" will return a promise:

await dispatch(fetchPostAction({ id: 'post-id' }))

The saga might look like:

import { call, takeEvery }        from 'redux-saga/effects'
import { implementPromiseAction } from '@adobe/redux-saga-promise'

import { fetchPostAction } from './actions'

function * fetchPostSaga(action) {
  yield call(implementPromiseAction, action, function * () {
    const { id } = action.payload
    return yield call(apiCallToFetchPost, id)
  })
}

export function * rootSaga() {
  yield takeEvery(fetchPostAction, fetchPostSaga);
}

It will resolve the promise with the value returned by apiCallToFetchPost or reject if apiCallToFetchPost throws an error. It also dispatches secondary actions with the resolution/rejection that you can access in a reducer. The package provides middleware you have to install to make it work.

(Disclaimer, I'm the author)

Scrubby answered 11/3, 2020 at 14:19 Comment(0)
U
1

I am the developer of @teroneko/redux-saga-promise. It was initially forked from @adobe/redux-saga-promise but now it has been completelly revamped to use createAction from @reduxjs/toolkit to support TypeScript.

To keep in touch with the example of @ronen, here the TypeScript equivalent.

Create promise action (creator):

import { promiseActionFactory } from '@teroneko/redux-saga-promise'
 
export const fetchPostAction = promiseActionFactory<void>().create<{ id: string }>('FETCH_POST')

To dispatch a promise action (from creator):

// promiseMiddleware is required and must be placed before sagaMiddleware!
const store = createStore(rootReducer, {}, compose(applyMiddleware(promiseMiddleware, sagaMiddleware)))
await store.dispatch(fetchPostAction({ id: 'post-id' }))

To resolve/reject the promise action (from saga):

import { call, takeEvery }        from 'redux-saga/effects'
import { implementPromiseAction } from '@teroneko/redux-saga-promise'

import { fetchPostAction } from './actions'

function * fetchPostSaga(action: typeof fetchPostAction.types.triggerAction) {
  yield call(implementPromiseAction, action, function * () {
    const { id } = action.payload
    return yield call(apiCallToFetchPost, id)
  })
  // or for better TypeScript-support
  yield call(fetchPostAction.sagas.implement, action, function * () {
    const { id } = action.payload
    return yield call(apiCallToFetchPost, id)
  })
}

export function * rootSaga() {
  yield takeEvery(fetchPostAction, fetchPostSaga);
}

So what's going on?

  1. promise action (creator) gets created
  2. promise action (from creator) gets created and
  3. dispatched to store.
  4. Then the promise action gets converted to a awaitable promise action where its deferred version is saved into the meta property. The action is immediatelly returned and
  5. passed to saga middleware.
  6. The now awaitable promise action is qualified to be used in implementPromiseAction that nothing else does than resolving or rejecting the deferred promise that is saved inside the meta property of the awaitable promise action.

See README for more features and advanced use cases.

Unlearn answered 3/12, 2021 at 0:36 Comment(0)
E
-1

Another solution

onSubmit: (values) => {
  return new Promise((resolve, reject) => {
    dispatch(someActionCreator({ values, resolve, reject }))
  });
}

In saga:

function* saga() {
  while (true) {
    const { payload: { values, resolve, reject } } = yield take(TYPE)
    // use resolve() or reject() here
  }
}

Reference: https://github.com/redux-saga/redux-saga/issues/161#issuecomment-191312502

Emmeram answered 11/1, 2021 at 13:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.