1

I use Redux-Saga in situations where there is complex business logic that is best separated from the component. We are adopting RTK Query and aren't able to use the endpoints manually. Yes, I am aware that Redux best practices docs suggest using Thunk when at all possible.

This particular saga task example doesn't make a great case for using Redux-Saga but there are definitely situations where the business logic is so involved/long/complex that it doesn't belong in a component and where we use saga functionality that can't be (elegantly) emulated with a simple Thunk. In this example I want to make a simple mutation (post request) on the backend:

export function* addNewCustomerTask(action: ReturnType<typeof addNewCustomer>) {
  const { name, industry, defaultIdentifierObfuscationLevel } = action.payload

  if (!name) {
    yield put(setToast({ text: 'Name can not be empty', style: ToastStyle.Error }))
  }
  else if (!industry) {
    yield put(setToast({ text: 'Industry can not be empty', style: ToastStyle.Error }))
  }
  else if (!defaultIdentifierObfuscationLevel) {
    yield put(setToast({ text: 'Default identifier obfuscation level can not be empty', style: ToastStyle.Error }))
  }
  else {
    try {
      yield call(authApiEndpoints.addCustomer.initiate, action.payload)
    }
    catch  {
      console.error('Error')
    }
  }
}

The yield call(authApiEndpoints.addCustomer.initiate, action.payload) statement doesn't do anything.

How do you perform a mutation inside of a Saga?

agusterodin
  • 417
  • 1
  • 5
  • 17

2 Answers2

2

It's an action creator, you need to put it's execution result.

// start it
const promise = yield put(authApiEndpoints.addCustomer.initiate(action.payload))
// wait until finished
yield promise;

// do something with it

// unsubscribe data - will be removed from store.
promise.unsubscribe()

As for the typings:You might need to extend the types of put for typed-redux-saga something like that: (I couldn't test it, so if you have to fix something up please submit an edit to this answer) Based on https://github.com/agiledigital/typed-redux-saga/blob/591955fa5bdd73ae6f668f27a049fde21a7ffb6f/types/index.d.ts#L175-L177 and https://github.com/reduxjs/redux-thunk/blob/290acf90fa5afac5b49f286bb3d8fc51aa864ed3/src/index.d.ts#L25-L27

declare module 'typed-redux-saga' {

  export function put<TReturnType>(
    thunkAction: ThunkAction<TReturnType, TState, TExtraThunkArg, TBasicAction>,
  ): SagaGenerator<TReturnType, PutEffect<TReturnType>>;

}
phry
  • 35,762
  • 5
  • 67
  • 81
  • Dope. If I use TS ignore, this works. Unfortunately I get compilation error ```Argument of type 'ThunkAction, string, void, "api">>, any, any, AnyAction>' is not assignable to parameter of type 'Action'. ``` I am using typed-redux-saga which helps some with type inference. – agusterodin Aug 12 '21 at 17:49
  • I also notice that the promise type is 'any'. With my old approach (non-RTK) I was able to infer the return type of the backend response. – agusterodin Aug 12 '21 at 17:50
  • Also putting this in a try catch block doesn't work which means there is no way to catch errors – agusterodin Aug 12 '21 at 19:19
  • 1
    You should get something catchable if you `yield promise.unwrap()`. I added an edit with a hint on how fixing types might look like. – phry Aug 12 '21 at 22:44
  • I wish I wasn't so clueless about how ThunkActions are typed and how they worked and knew how to fix the types. Total black box for me. Extending the type as shown above doesn't appear to work. The fact that I use typed-redux-saga's macro variants (typed-redux-saga/macro) to undo their proxies at build time makes things even more complicated. Really wish there was a way to elegantly convert an endpoint initiator to a promise and use endpoints in saga the way I used to (for fallback scenarios where not viable to put logic directly in component). – agusterodin Aug 17 '21 at 15:29
  • Thinking something along lines of Usage: yield call(convertEndpointToPromise(authApiEndpoints.addCustomer), action.payload) Helper: Where convertEndpointToPromise is a function that would promisify the entire dispatching of action, unwrapping, and unsubscribing process. Store could be accessed by using store.dispatch() directly inside of the helper's source code. – agusterodin Aug 17 '21 at 15:31
  • Is this barking up the right tree or does it seem like a bad idea? Would TS typing being viable (and not too painful to set up?). Is this something that could make its way to future versions of RTK (probably not)? – agusterodin Aug 17 '21 at 15:32
  • We are not going to add support for sagas as for almost all use cases we recommend not using them. And your `convertEndpointToPromise` is exactly what `dispatch(initiate(...))` does. Dispatching a function makes the dispatch return the return type of said function. Best you could do would be to ask `typed-redux-saga` to add support for enhancing saga with middleware or even add typings for thunk. – phry Aug 17 '21 at 15:47
2

If you want to use RTK-Query inside Redux-Saga and have no issue with Typescript you need to use both put and call then you can use select and RTK-query selector to access the data.

const promise = yield put(
  yield call(authApiEndpoints.addCustomer.initiate, action.payload)
);
// You need to wait for the query to be done
yield promise
    
const { data } = yield select(authApiEndpoints.addCustomer.select());
Gabriel Diez
  • 1,648
  • 2
  • 17
  • 26