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)
}
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:
pending = truesubmitted = true (also sets pending = false)validity = response (format however you'd like)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..! 馃槃
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:
and the onSubmit:
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..! 馃槃