Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: Integration with redux-form #161

Closed
danturu opened this issue Feb 29, 2016 · 31 comments
Closed

Question: Integration with redux-form #161

danturu opened this issue Feb 29, 2016 · 31 comments

Comments

@danturu
Copy link

danturu commented Feb 29, 2016

Can't figure out the onSubmit method. Let's say I have the following form:

const formConfig = { form: 'form', fields: ['name',] }

const stateToProps = (state) => {
  return state;
}

const dispatchToProps = (dispatch) => {
  return {
    onSubmit: ({ name }) => {
      // Should return a promise
    }
  }
}

@reduxForm(formConfig, stateToProps, dispatchToProps)
export class Todos extends React.Component {
  render() {
    const { fields: { name }, error, handleSubmit } = this.props;

    return (
      <form  onSubmit={handleSubmit}>
        <input type="text" {...name} />
        <button type="submit">Create</button>
        {error && <div>{error}</div>}
      </form>
    );
  }
}

From the redux-form docs:

If your onSubmit function returns a promise, the submitting property will be set to true until the promise has been resolved or rejected. If it is rejected with an object matching { field1: 'error', field2: 'error' } then the submission errors will be added to each field (to the error prop) just like async validation errors are. If there is an error that is not specific to any field, but applicable to the entire form, you may pass that as if it were the error for a field called _error, and it will be given as the error prop.

The saga could look like:

function* createTodo(action) {
   try {
      const todo = yield call(Api.createTodo, action.name);
      yield put({ type: 'CREATE_TODO_SUCCESS', todo });
   } catch (e) {
      yield put({ type: 'CREATE_TODO_FAILURE', reason: e.toString() });
   }
}

function* saga() {
  yield* takeEvery('CREATE_TODO', createTodo);
}

Then:

onSubmit: ({ name }) => {
  return new Promise((resolve, reject) => {
    dispatch({ type: 'CREATE_TODO', name });      
  });
}

How could I intercept the CREATE_TODO_SUCCESS and CREATE_TODO_FAILURE actions in the promise? I'm really stuck here.

/cc @erikras

@danturu
Copy link
Author

danturu commented Mar 1, 2016

A temporary solution could be:

import { stopSubmit } from 'redux-form';

...

onSubmit: ({ name }) => {
  setTimeout(() => dispatch({ type: 'CREATE_TODO', name }));  

  return new Promise(() => {}); // Set the form state to 'SUBMITTING'
}

...

function* createTodo(action) {
   try {
      const todo = yield call(Api.createTodo, action.name);
      yield put({ type: 'CREATE_TODO_SUCCESS', todo });
   } catch (e) {
      yield put({ type: 'CREATE_TODO_FAILURE', reason: e.toString() });
   }
}

function* watchTodo() {
  while (true) {
    yield call(createTodo, yield take('CREATE_TODO'));
  }
}

function* watchForm() {
  while (true) {
    const { failure } = yield race({ 
      success: take(CREATE_TODO_SUCCESS),
      failure: take(CREATE_TODO_FAILURE)
    });

    if (failure) {
      yield put(stopSubmit('form', { _error: failure.reason.message }));
    } else {
      yield put(stopSubmit('form'));
    }
  }
}

function* saga() {
  yield [
    fork(watchTodo),
    fork(watchForm),
  ]
}

@erikras
Copy link

erikras commented Mar 1, 2016

I have not fully wrapped my head around redux-saga yet. What sort of API changes from redux-form would make integration with redux-saga easier? e.g. callbacks instead of promises?

@danturu
Copy link
Author

danturu commented Mar 1, 2016

@yelouafi Could you please change the label to 'feedback wanted'.

@tokenvolt
Copy link

@rosendi
I'm doing this way.
In the form component:

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

@yelouafi yelouafi closed this as completed Mar 8, 2016
@musbaig
Copy link

musbaig commented Apr 8, 2016

@tokenvolt brilliant! Thanks so much for sharing that tip, totally solved the problem. You should blog/gist it somewhere. May be do an add-a-note PR here or over at redux-form. It really helps address the interaction between redux-sagas and redux-form.

@timothyallan
Copy link

timothyallan commented Jun 15, 2016

A 'gotcha' for the solution via @tokenvolt is that connect must be first, (or last depending how you look at it!), in the chain if you are using it. i.e.

export default (connect(
  mapStateToProps, mapDispatchToProps)(
  reduxForm(
    { form: 'loginForm', validate }
  )(LoginPage)));

works great, whereas

export default reduxForm({ form: 'loginForm', validate })(
    connect(mapStateToProps, mapDispatchToProps
   ) ((LoginPage)));

will error on submit asking for a handleSubmit() function.

@musbaig
Copy link

musbaig commented Jun 15, 2016

redux-form wraps and exposes connect(), http://redux-form.com/5.2.5/#/api/reduxForm?_k=myjcva, so you technically don't need to worry about order, but, YMMV.

@timothyallan
Copy link

timothyallan commented Jun 15, 2016

Ahh right, I see that the older version 5 exposes (or at least replicates connect()). Version 6 doesn't.

@musbaig
Copy link

musbaig commented Jun 15, 2016

@timothyallan really? Interesting, wonder what the new mechanism is, or lack thereof. Thanks for the heads up!

@timothyallan
Copy link

It's much nicer actually :) http://redux-form.com/6.0.0-alpha.15/docs/api/ReduxForm.md/

@igorbarbashin
Copy link

igorbarbashin commented Jun 21, 2016

@tokenvolt Thanks, it kinda works. I see the STOP_SUBMIT action.
But how do I pass validation errors?

I tried

reject({ username: 'Username doesn\'t exist' });
yield call(reject, 'Username doesn\'t exist');

None of them worked. (I'm on redux-form 6 alpha)

For now my solution is to use stopSubmit action creator

import { stopSubmit } from 'redux-form';
. . . 
yield put(stopSubmit('Login', { username: 'Username doesn\'t exist', password: 'And your password sucks' }));

Is there a better solution?

@igorbarbashin
Copy link

igorbarbashin commented Jun 21, 2016

Nevermind, I've figured it out.

This is the right way to do it:

import { SubmissionError } from 'redux-form';
. . .
reject(new SubmissionError({ username: 'Username doesn\'t exist', password: 'Please enter your password' }));

// or yield for better testability
yield call(reject, new SubmissionError({ username: 'Username doesn\'t exist', password: 'Please enter your password' }));

Thanks for your comment, it helped a lot!

@angelyordanov
Copy link

I've refined the solution of @tokenvolt with the help of a saga that would listen for success/failure actions and resolve/reject a promise returned to the redux-form. The idea being that one needs to just tell the form what action it should dispatch on submit and what actions it should wait for on success or failure.

It's used like that.

export const LoginForm = reduxForm({
  form: 'loginForm',
  fields: ['email', 'password'],
  onSubmit: onSubmitActions(LOGINFORM_SUBMIT, LOGINFORM_SUCCESS, LOGINFORM_FAILURE),
})(LoginFormComponent);
function* loginSaga() {
  const { payload: { email, password } } = yield take(LOGINFORM_SUBMIT);
  // check if the user/pass are correct
  // ....
  if (success) {
    yield put(LOGINFORM_SUCCESS);
  } else {
    yield put(LOGINFORM_FAILURE, { _error: 'Incorrect user/password' });
  }
}

The submit action's payload is the fields object and the payload for the failure action is the field errors object expected from redux-form.

And here is the full onSubmitActions code.

import { put, take, race } from 'redux-saga/effects';
import { takeEvery } from 'redux-saga';

export const FORM_SUBMIT = 'redux-form-submit-actions/FORM_SUBMIT';

export function formSubmit(submitAction, successAction, failureAction, values, resolve, reject) {
  return {
    type: FORM_SUBMIT,
    payload: {
      submitAction,
      successAction,
      failureAction,
      values,
      resolve,
      reject,
    },
  };
}

export function onSubmitActions(submitAction, successAction, failureAction) {
  return (values, dispatch) =>
    new Promise((resolve, reject) => {
      dispatch(formSubmit(submitAction, successAction, failureAction, values, resolve, reject));
    });
}

function* formSubmitSaga({
  payload: {
    submitAction,
    successAction,
    failureAction,
    values,
    resolve,
    reject,
  },
}) {
  yield put({ type: submitAction, payload: { ...values } });

  const { success, failure } = yield race({
    success: take(successAction),
    failure: take(failureAction),
  });

  if (success) {
    resolve();
  } else {
    reject(failure.payload);
  }
}

export function* watchFormSubmitSaga() {
  yield* takeEvery(FORM_SUBMIT, formSubmitSaga);
}

@bghveding
Copy link

@angelyordanov This works great, thanks :)

Do note that V6 of redux-form expects an instance of SubmissionError for errors now (http://redux-form.com/6.0.0-alpha.15/docs/api/SubmissionError.md/)

e.g.
reject(new SubmissionError(failure.payload));

@jcheroske
Copy link

jcheroske commented Jul 2, 2016

Using redux-actions, I created this simple action creator to help with @tokenvolt's pattern:

import { identity, noop } from 'lodash'
import { createAction } from 'redux-actions'

const payloadCreator = identity;
const metaCreator = (_, resolve = noop, reject = noop) => ({ resolve, reject });

export const promiseAction = (type) => createAction( type, payloadCreator, metaCreator );

This allows actions to be fired with just a quick:

dispatch(someActionCreator(payload, resolve, reject));

The saga signature then simply looks like:

function* handleSomeAction({ payload, meta: {resolve, reject} }) {

Because there are noop resolve and reject default functions, two things get simpler:

  • The action creator can be used outside of a promise context.
  • The saga is free to blindly call the promise methods

So, calling the action creator without the promise still works:

dispatch(someActionCreator(payload));

function* handleSomeAction({ payload, meta: {resolve, reject} }) {
  try {
    yield call(doTheThing);
    yield call(resolve); 
  } catch (error) {
    yield call(reject, error); 
  }
}

The last piece is a little function that that I called bindActionToPromise (which might be a really sucky name):

export const bindActionToPromise = (dispatch, actionCreator) => payload => {
  return new Promise( (resolve, reject) => dispatch( actionCreator(payload, resolve, reject) ) );
};

The complete pattern then looks like:

const someActionCreator = promiseAction(SOME_ACTION_TYPE);

const mapDispatchToProps = (dispatch) => ({
  onSubmit: bindActionToPromise(dispatch, someActionCreator)
});

function* watchSomeAction() {
  yield* takeEvery(SOME_ACTION_TYPE, handleSomeAction);
}

function* handleSomeAction({ payload, meta: {resolve, reject} }) {
  try {
    yield call(doTheThing);
    yield call(resolve); 
  } catch (error) {
    yield call(reject, error); // Convert to SubmissionError here if needed
  }
}

Hope this helps someone.

@mykyta-shulipa
Copy link

Did someone try redux-form-submit-saga lib?

@anykao
Copy link

anykao commented Oct 12, 2016

@nktssh I have tried and worked pretty well.

@oanylund
Copy link

I do not know if these action creators are new or have some downside, but could we not just use the startSubmit and stopSubmit action creators and pass the form name with the submit action?

Example:

function* createEntity(entity, apiFn, formId, newEntity) {
  yield put( entity.request() )
  yield put( startSubmit(formId) )
  const {response, error} = yield call(apiFn, newEntity)
  if(response) {
    yield put( entity.success(response) )
    yield put( reset(formId) )
    yield put( stopSubmit(formId) )
  }
  else {
    yield put( entity.failure(error) )
    // handle and format errors from api
    yield put( stopSubmit(formId, serverValidationErrors) )
  }
}

Quite new to both redux-form and redux-saga, so there could be something i am missing here.

@Andarist
Copy link
Member

@oanylund
I think thats the issue for redux-form rather than the redux-saga, so it should be issued on their repository.

@oanylund
Copy link

Agreed. I just came across this issue when i was facing the same challenge and thought i would share the solution i ended up with.

@gustavohenke
Copy link

gustavohenke commented Oct 24, 2016

After giving a quick look around this issue, I think the solution by @oanylund is the cleanest possible.
It doesn't requires anything that looks as hacky as passing resolve and reject around.
I already have my form name as a constant, therefore it's so easy to issue startSubmit and stopSubmit actions!

@mykyta-shulipa
Copy link

@oanylund but what about validation when clicking 'Submit'?
Some forms could has complex validation flow, and part of this flow must be executed only when user hit 'Submit' button...

@oanylund
Copy link

Seems like this is not the proper place to discuss this, but to answer shortly...
As i understand startSubmit, it only sets the submitting flag in the form reducer to true and nothing else. I only use it to disable the submit button and set the fields to readonly while submitting. The sync validation happens before this when you click your submit button as always. So if your sync validation is not passing, the action to fire the saga will never be dispatched.

stopSubmit sets the submitting flag to false, and you can also pass a new error object with your server errors to the form.

@kevinmichaelchen
Copy link

@nktssh if you want to do client-side validation before you put the submit action, you can mod the answer given by @angelyordanov by tweaking the onSubmitActions function to take a validation function as a parameter (e.g., validateFn), which you invoke before submitting the form. If there are any validation errors, you can call

// validateFn returns an empty object if no errors
// otherwise, an object like { _error: 'errorMessage' }
// where _error is the redux-form key for form-wide errors
const errors = validateFn(values);
if (errors) reject(new SubmissionError(errors))

@andrewsuzuki
Copy link

andrewsuzuki commented Dec 28, 2016

Here's my variation on @oanylund's answer. I have a module with a utility called formSaga:

import { put, call } from 'redux-saga/effects'
import { startSubmit, stopSubmit, reset, SubmissionError } from 'redux-form'


export default function* formSaga(formId, apiSaga, ...apiSagaArgs) {
  // Start form submit
  yield put(startSubmit(formId))

  try {
    yield call(apiSaga, ...apiSagaArgs)

    // Success

    yield put(reset(formId))
    yield put(stopSubmit(formId))
  } catch (err) {
    if (err instanceof SubmissionError) {
      yield put(stopSubmit(formId, err.errors))
    } else {
      console.error(err) // eslint-disable-line no-console
      yield put(stopSubmit(formId, { _error: err.message }))
    }
  }
}

Example usage: for something like login, I want to be able to login either with an action directly (login/loginSaga), or through a form submission (submitLoginForm/submitLoginFormSaga).

// Actions

export const login = createAction('Login', (email, password) => ({ email, password }))
export const submitLoginForm = createAction('Submit Login Form', (fields) => fields)


// Sagas

export function* loginSaga({ payload: { email, password } }) {
  // call api, etc...
  try {
    yield call(request, { endpoint: '/auth/login', method: 'post', data: { email, password } })
  } catch (e) {
    // could throw a redux-form/SubmissionError here and it'll show up in the view!
    // etc...
  }
}


export function* submitLoginFormSaga({ payload: { email, password } }) {
  // the ...apiSagaArgs in the util keep things pretty generic, but in this case
  // (and most cases) we can just generate a "fake" action using the action creator
  yield call(formSaga, 'login', loginSaga, login(email, password))
}


export function* watcherSaga() {
  yield [
    takeLatest(login.getType(), loginSaga),
    takeLatest(submitLoginForm.getType(), submitLoginFormSaga),
  ]
}

** Might not be idiomatic, this is my first day with redux-saga.

@babsonmatt
Copy link

@andrewsuzuki How are you passing values from your form to your saga?

@briska
Copy link

briska commented Feb 20, 2017

i really think this line of code could be removed: redux-form/redux-form@7e07256#diff-28d8f38ee02f29d2bc406450f6c0d870R27 or at least could be added some option to turn this automatic setSubmitSuccess() off...

@mykyta-shulipa
Copy link

mykyta-shulipa commented Feb 20, 2017

i really think this line of code could be removed: redux-form/redux-form@7e07256#diff-28d8f38ee02f29d2bc406450f6c0d870R27 or at least could be added some option to turn this automatic setSubmitSuccess() off...

+1 for new option to not calling stopSubmit and setSubmitSuccess!

Maybe, we can do this as plugin?

@alpox
Copy link

alpox commented Jul 11, 2017

I thought that maybe someone would be interested in my solution of this. I built it through the base pattern of @jcheroske - thanks for this!

My solution mainly consists of some helper functions. (I wrote it in Typescript, so you may have to read a bit around type declarations)

I created the same metaCreator for the use with redux-actions:

export function metaCreator() {
    const [resolve, reject] = [...arguments].slice(-2);
    return { resolve, reject };
}

Then I could create the equivalent of createAction of redux-actions which makes use of createAction internally, but makes it return a promise and injects resolve and reject into the meta field of the FSA compilant action:

export const createPromiseAction = (
    actionType: string,
    payloadCreator: (...args: any[]) => any = identity,
) => {
    const action = (...args: any[]) => (
        dispatch: Dispatch<any>,
    ): Promise<any> =>
        new Promise((resolve, reject) => {
            dispatch(
                createAction(actionType, payloadCreator, metaCreator)(
                    ...args,
                    resolve,
                    reject,
                ),
            );
        });

    (action as any)[Symbol.toPrimitive] = () => actionType;
    (action as any).toString = () => actionType;
    return action;
};

Edit: for this to work you need redux-thunk middleware to route the promise through the dispatch call

This can be used simply instead of createAction:

export const login = createPromiseAction(AuthActionType.Login);

In addition, i created another helper - a function which wraps the original saga and catches errors from the original saga and rejects the error through the promise. It calls resolve, when the saga finished its work:

export function formSaga(originalSaga: (...args: any[]) => any) {
    function* newSaga({
        meta: { resolve, reject },
        ...action,
    }: {
        meta: {
            resolve: () => any;
            reject: (err: any) => any;
        };
    }) {
        try {
            yield call(originalSaga, action);
            yield call(resolve);
        } catch (error) {
            yield call(reject, error);
        }
    }

    return newSaga;
}

This can be used like:

export function* authSaga() {
    yield takeEvery(login.toString(), formSaga(loginRequest));
}

Where login is the previously through createPromiseAction created action.

In loginRequest i can then just throw the submission error as i'm used to:

try { ... }
catch (err) {
        yield put(error(AuthActionType.Login, err));

        throw new SubmissionError({
            _error: err.response.data.error.fields || err.response.data.error,
        });
    }

I hope this helps somebody :-) thanks for your help to create this!

@borgateo
Copy link

Following your suggestions, we have created a simple helper:

export const bindActionToPromise = (dispatch, actionCreator) => payload => {
  return new Promise((resolve, reject) =>
    dispatch(actionCreator(payload, resolve, reject))
  )
}

Then we import it with a simple import { bindActionToPromise } from '../../utils' and use it this way:

const submit = (closePortal, values, dispatch) => {
  const submitUpdate = bindActionToPromise(dispatch, updateSomething.request)
  return submitUpdate({ data: values })
}

@superbiche
Copy link

superbiche commented Mar 6, 2018

In case someone's interested, here's A not-that-simple-but-fully-working-way of handling Redux Forms with Sagas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests