Ming Soon
Ming Soon

Reputation: 1018

Promises in redux-saga

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?

Upvotes: 12

Views: 35204

Answers (4)

Teneko
Teneko

Reputation: 1491

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.

Upvotes: 1

ridermansb
ridermansb

Reputation: 11059

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

Upvotes: -1

ronen
ronen

Reputation: 2598

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)

Upvotes: 4

jukben
jukben

Reputation: 1098

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);
}

Upvotes: 22

Related Questions