Redux and Forms and APIs, Oh My!
Many web applications require forms that are submitted to a back-end API.
If your web application is built using Redux, you might use redux-form to handle the forms and redux-api-middleware to handle the API calls.
And if you are using those libraries, you’ll soon find (as we did) that they don’t quite work together the way you’d like.
The Problem
redux-form is a really nice library for working with forms. It has a lot of great features, so we want to continue using it. redux-api-middleware is the same - a lot of great features that we want to continue to take advantage of. We needed to find a way to make them play nicely together.
The redux-form documentation says:
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.
Returning a promise from onSubmit
means that you get a pretty nice experience out of the box. You can use the resulting submitting
property to disable or otherwise change the style of your submit button. Rejecting the promise with errors formatted as above means that the form fields can show error information automatically.
The problem is that redux-api-middleware doesn’t work this way. It dispatches a request
action to the Redux store when submitting the request, and then dispatches either a success
or a failure
action upon completion. This is what you’d expect in a Redux context.
How do we get these two libraries to work together?
redux-form uses the Redux store as well, but how it does so appears to be an internal implementation detail rather than a documented part of the API.
It does provide a plugin mechanism, but each plugin is specific to a single form. There doesn’t seem to be a way to provide a general plugin that works with all forms. If I want a general mechanism that will work with all of my forms, plugins aren’t the solution unless I’m willing to list all of my forms when I’m initially creating my reducers. This is not tenable for large applications, or applications with dynamic forms.
The Solution
It turns out that redux-api-middleware returns a Promise when you dispatch a CALL_API
action. Using this promise, we can write an adapter that wraps around any form-submission action creator.
The Promise-returning behavior is not documented, so this solution could break in the future. Given the different options we looked at, this seemed to be the least bad.
Here’s the adapter we wrote. It’s part of our react-boilerplate project at Zeal.
This looks like a small amount of code, but there’s quite a bit going on here, and some of it is a bit subtle.
-
We want the wrapped action creator to take the same arguments as the original action creator, so the adapter returns a function that just passes on whatever arguments it was given (
...args
). -
We create and return a new Promise. This Promise is what redux-form sees so it triggers the behavior I described above.
-
In the body of our Promise, we
dispatch
the original action creator with its arguments. As discussed above, it returns its own Promise. -
When the redux-api-middleware Promise resolves, we look at the response. If it contains an
error
property, we know from the documentation that the API call failed, so wereject
our promise. If there is noerror
property, weresolve
our Promise with the response. -
When
reject
ing our Promise, we have an opportunity to reformat the API response into the error object structure that redux-form expects. I haven’t shown an implementation here, because it will depend on what your back-end API returns.
Here’s how we use the adapter:
You can see the use of the formApiAdapter
inside of mapDispatchToProps
. I’ve included an additional action creator in order to show how you can pass along other non-form actions to your component.
So far, our forms have been embedded within outer components like this, so this is how we use the adapter. Since reduxForm
takes all of the same arguments as connect
does, you can use the adapter in that context as well.
NOTE: This approach works with redux-form versions 4 and 5. As I write this, redux-form version 6 is in the works and makes some significant changes that may make some of what I say here obsolete.
UPDATE: redux-form 6 requires only a small change to the adapter. See Update: Redux Form 6 and APIs for details.