React-redux-form: Redux-saga: Sanity check

Created on 20 Dec 2016  路  3Comments  路  Source: davidkpiano/react-redux-form

So i've been trying to get rrf to play nicely with redux-saga, and it's been a little tricky.

As far as i've understood, you should utilize the actions.submit action creator, and put a thunked action creator as the second param.
Then the thunked action creator should provide a promise to redux-saga, that it either resolves or rejects...

I'm just checking here. I've got it working locally, and i've made a helper to do this if anybody is interested.

@davidkpiano would love your opinion on the example.

The helper:

function rrfSubmitCreator (form, action) {
  // The outer function is the decorated action creator
  // It grabs all arguments from the original action creator,
  // and makes them available for the promise
  return (...actionArgs) => {
    // Return normal thunk
    return (dispatch) => {
      // Make a promise that sends resolve and reject to redux-saga
      const promise = new Promise((resolve, reject) => {
        // Get the result from the original action
        const actionResult = action(...actionArgs)
        // Enhance it with resolve and reject meta
        const enhancedAction = Object.assign(actionResult, {meta: {resolve, reject}})
        // Dispatch the action
        dispatch(enhancedAction)
      }).then((result) => {
        // When the form it submitted successfully,
        // set it back to pristine
        dispatch(actions.setPristine(form))
        return result
      })
      // Set the form in submission mode
      return dispatch(actions.submit(form, promise))
    }
  }
}

Initializing the action creator like this:

export const actionCreator = rrfSubmitCreator('form', (foo, bar) => {
  return {
    type: FORM_SUBMISSION,
    foo,
    bar
  }
})

And the saga should work generally like this:

export function* saga (action) {
  yield put({type: REQUEST})
  try {
    const {body} = action
    const payload = yield call(api, '/api/foo/bar', body)
    yield put({type: RESPONSE_SUCCESS, payload})
    action.meta.resolve(payload)
    return payload
  } catch (error) {
    yield put({type: RESPONSE_FAILURE, error: error.message})
    action.meta.reject(error.message)
    throw error
  }
}
function* main() {
  yield takeLatest(FORM_SUBMISSION, saga)
}
question

Most helpful comment

okay, after looking at your example again i decided to take another approach.

i made a dedicated saga just for submitting forms (or maybe i just misunderstood your example), anyway, not it works like this:

const SUBMIT_FORM = 'SUBMIT_FORM'
const SUBMIT_FORM_SUCCESS = 'SUBMIT_FORM_SUCCESS'
const SUBMIT_FORM_FAILURE = 'SUBMIT_FORM_FAILURE'

// Action creator
export function submitForm (model, action) {
  return {
    type: SUBMIT_FORM,
    model,
    action
  }
}

// Saga
function* submitFormSaga ({model, action}) {
  // Set the form as pending
  yield put(actions.setPending(model, true))
  try {
    // Dispatch the original action
    const request = yield put(action)
    // Listen for the response from the action
    const {result, error} = yield race({
      result: take(`${request.type}_SUCCESS`),
      error: take(`${request.type}_FAILURE`)
    })
    if (result) {
      // When receiving the result set the form as submitted
      // load the most recent data into the form
      // and set is as pristine
      yield put(actions.setSubmitted(model, true))
      yield put(actions.load(model, result.payload))
      yield put(actions.setPristine(model))
      yield put({type: SUBMIT_FORM_SUCCESS, model})
      return result.payload
    } else {
      // If the result is an error
      // set the form is an error state and update field validity
      yield put(actions.setSubmitFailed(model))
      yield put(actions.setValidity(model, error))
      yield put({type: SUBMIT_FORM_FAILURE, model})
    }
  } catch (error) {
    throw error
  }
}

export default function* () {
  yield takeEvery(SUBMIT_FORM, submitFormSaga)
}

and the onSubmit:

onSubmit (values) {
  this.props.submitForm('form', asyncActionCreator(values))
}

I altered your example a bit. In my setup the setValidity upon success doesn't really apply, since my api only returns successful when is as performed the requested action. I also wanted to ensure the form has the most recent data returned by the api, hence the const result = yield take(${requestType}_SUCCESS) trick, (which fit i my naming convention, but is not generally applicable)..

Anyway it seems it's quite possible to make a quite lean setup after all, awesome..! 馃槃

All 3 comments

Hmm, interesting - although, is imperatively calling action.meta.resolve(payload) "allowed" in redux-saga? I thought that all side-effects should be captured with yield call(...).

So the general idea to translate the submit() thunk to a saga, if I'm not mistaken, is something like this:

function* submitSaga({model, promise}) {
  yield put(actions.setPending(model, true));

  try {
    const validity = yield promise;
    yield put(actions.setSubmitted(model, true));
    yield put(actions.setValidity(model, validity));
  } catch(error) {
    yield put(actions.setSubmitFailed(model));
    yield put(actions.setValidity(model, error));
    throw error;
  }
}

Would this general structure also apply to your use case? This is just a direct translation from the original submit action. In short, this is what happens:

  1. pending = true
  2. wait for the promise to resolve...

    • If resolved, submitted = true (also sets pending = false)

    • validity = response (format however you'd like)

    • If rejected, submitFailed = true (also sets pending = false)

    • validity = error (format however you'd like)

You could call the resolve / reject with yield call(...), but as to if its imperative of not i actually don't know. It doesn't really change much, other than the stops the function execution until the yielded call returns (and in the case here i don't see that it is paramount).

My purpose with this was to try to wrap the state changes of the form upon submission, so you could dispatch as normal (as in simple object returning) action in the onSubmit callback of the form.

Is amounts to some boilerplate either way because executing the actions for the form submission state is not really natural in the flow (meaning that after the onsubmit is called, the form is unaware of the subsequent fate of the action dispatched in that callback.

In my current setup the whole flow looks like this:

onSubmit: trigger main action creator eg: this.props.dispatch(submit(values))
-> submit is the wrapped action creator with the promise which triggers
 -> actions.submit with the promise that sends
  -> the original action, which is picked up by
   -> a takeLatest in the saga. this triggers the first saga
    -> with a try / catch for the overall execution. it handles fx. redirecting upon success, showing toast messages eg. is yields the ajax saga
     -> which then actually makes the call to the server.

So it's a pretty big russian doll setup already.

I've adapted you example to omit promise all together, that simplifies it a bit in my setup, and works as my wrapper (except it does not set the form to pristine on successful submission, but that's more of a preference that can be omitted

function* submitSaga (model, saga, ...args) {
  yield put(actions.setPending(model, true))

  try {
    const validity = yield call(saga, ...args)
    yield put(actions.setSubmitted(model, true))
    yield put(actions.setValidity(model, validity))
    return validity
  } catch (error) {
    yield put(actions.setSubmitFailed(model))
    yield put(actions.setValidity(model, error))
    throw error
  }
}

I'm trying to make the process of updating form state so invisible as possible, and wrapping the action creator as a thunk seems kinda laborious when it really can be omitted. but on the other hand, using the saga approach means i have to adapt all sagas to use the new wrapper saga, and it seems kinda the wrong place to do it...

okay, after looking at your example again i decided to take another approach.

i made a dedicated saga just for submitting forms (or maybe i just misunderstood your example), anyway, not it works like this:

const SUBMIT_FORM = 'SUBMIT_FORM'
const SUBMIT_FORM_SUCCESS = 'SUBMIT_FORM_SUCCESS'
const SUBMIT_FORM_FAILURE = 'SUBMIT_FORM_FAILURE'

// Action creator
export function submitForm (model, action) {
  return {
    type: SUBMIT_FORM,
    model,
    action
  }
}

// Saga
function* submitFormSaga ({model, action}) {
  // Set the form as pending
  yield put(actions.setPending(model, true))
  try {
    // Dispatch the original action
    const request = yield put(action)
    // Listen for the response from the action
    const {result, error} = yield race({
      result: take(`${request.type}_SUCCESS`),
      error: take(`${request.type}_FAILURE`)
    })
    if (result) {
      // When receiving the result set the form as submitted
      // load the most recent data into the form
      // and set is as pristine
      yield put(actions.setSubmitted(model, true))
      yield put(actions.load(model, result.payload))
      yield put(actions.setPristine(model))
      yield put({type: SUBMIT_FORM_SUCCESS, model})
      return result.payload
    } else {
      // If the result is an error
      // set the form is an error state and update field validity
      yield put(actions.setSubmitFailed(model))
      yield put(actions.setValidity(model, error))
      yield put({type: SUBMIT_FORM_FAILURE, model})
    }
  } catch (error) {
    throw error
  }
}

export default function* () {
  yield takeEvery(SUBMIT_FORM, submitFormSaga)
}

and the onSubmit:

onSubmit (values) {
  this.props.submitForm('form', asyncActionCreator(values))
}

I altered your example a bit. In my setup the setValidity upon success doesn't really apply, since my api only returns successful when is as performed the requested action. I also wanted to ensure the form has the most recent data returned by the api, hence the const result = yield take(${requestType}_SUCCESS) trick, (which fit i my naming convention, but is not generally applicable)..

Anyway it seems it's quite possible to make a quite lean setup after all, awesome..! 馃槃

Was this page helpful?
0 / 5 - 0 ratings