react / redux-form: how to return promise from onSubmit?

x-ray picture x-ray · Jan 8, 2016 · Viewed 9.6k times · Source

I'm trying to wrap my head around redux, react-redux and redux-form.

I have setup a store and added the reducer from redux-form. My form component looks like this:

LoginForm

import React, {Component, PropTypes} from 'react'
import { reduxForm } from 'redux-form'
import { login } from '../../actions/authActions'

const fields = ['username', 'password'];

class LoginForm extends Component {
    onSubmit (formData, dispatch) {
        dispatch(login(formData))
    }

    render() {
        const {
            fields: { username, password },
            handleSubmit,
            submitting
            } = this.props;

        return (
            <form onSubmit={handleSubmit(this.onSubmit)}>
                <input type="username" placeholder="Username / Email address" {...username} />
                <input type="password" placeholder="Password" {...password} />
                <input type="submit" disabled={submitting} value="Login" />
            </form>
        )
    }
}
LoginForm.propTypes = {
    fields: PropTypes.object.isRequired,
    handleSubmit: PropTypes.func.isRequired,
    submitting: PropTypes.bool.isRequired
}

export default reduxForm({
    form: 'login',
    fields
})(LoginForm)

This works as expected, in redux DevTools I can see how the store is updated on form input and on submitting the form the login action creator dispatches the login actions.

I added the redux-thunk middleware to the store and setup the action creator(s) for logging in as described in the redux docs for Async Actions:

authActions.js

import ApiClient from '../apiClient'

const apiClient = new ApiClient()

export const LOGIN_REQUEST = 'LOGIN_REQUEST'
function requestLogin(credentials) {
    return {
        type: LOGIN_REQUEST,
        credentials
    }
}

export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
function loginSuccess(authToken) {
    return {
        type: LOGIN_SUCCESS,
        authToken
    }
}

export const LOGIN_FAILURE = 'LOGIN_FAILURE'
function loginFailure(error) {
    return {
        type: LOGIN_FAILURE,
        error
    }
}

// thunk action creator returns a function
export function login(credentials) {
    return dispatch => {
        // update app state: requesting login
        dispatch(requestLogin(credentials))

        // try to log in
        apiClient.login(credentials)
            .then(authToken => dispatch(loginSuccess(authToken)))
            .catch(error => dispatch(loginFailure(error)))
    }
}

Again, in redux DevTools I can see that this works as expected. When dispatch(login(formData)) is called in onSubmit in the LoginForm, first the LOGIN_REQUEST action is dispatched, followed by LOGIN_SUCCESS or LOGIN_FAILURE. LOGIN_REQUEST will add a property state.auth.pending = true to the store, LOGIN_SUCCESS and LOGIN_FAILURE will remove this property. (I know this might me something to use reselect for, but for now I want to keep it simple.

Now, in the redux-form docs I read that I can return a promise from onSubmit to update the form state (submitting, error). But I'm not sure what's the correct way to do this. dispatch(login(formData)) returns undefined.

I could exchange the state.auth.pending flag in the store with a variable like state.auth.status with the values requested, success and failure (and again, I could probably use reselect or something alike for this).

I could then subscribe to the store in onSubmit and handle changes to state.auth.status like this:

// ...

class LoginForm extends Component {
    constructor (props) {
        super(props)
        this.onSubmit = this.onSubmit.bind(this)
    }
    onSubmit (formData, dispatch) {
        const { store } = this.context
        return new Promise((resolve, reject) => {
            const unsubscribe = store.subscribe(() => {
                const state = store.getState()
                const status = state.auth.status

                if (status === 'success' || status === 'failure') {
                    unsubscribe()
                    status === 'success' ? resolve() : reject(state.auth.error)
                }
            })
            dispatch(login(formData))
        }).bind(this)
    }

    // ...
}
// ...
LoginForm.contextTypes = {
    store: PropTypes.object.isRequired
}

// ...

However, this solution doesn't feel good and I'm not sure if it will always work as expected when the app grows and more actions might be dispatched from other sources.

Another solution I have seen is moving the api call (which returns a promise) to onSubmit, but I would like to keep it seperated from the React component.

Any advice on this?

Answer

Michelle Tilley picture Michelle Tilley · Jan 8, 2016

dispatch(login(formData)) returns undefined

Based on the docs for redux-thunk:

Any return value from the inner function will be available as the return value of dispatch itself.

So, you'd want something like

// thunk action creator returns a function
export function login(credentials) {
    return dispatch => {
        // update app state: requesting login
        dispatch(requestLogin(credentials))

        // try to log in
        apiClient.login(credentials)
            .then(authToken => dispatch(loginSuccess(authToken)))
            .catch(error => dispatch(loginFailure(error)))

        return promiseOfSomeSort;
    }
}