IMPORTANT This post is outdated, but I will leave it here for posterity.
React v15 was released before I could get the next rewrite of redux-form
published, so I had to release v5
as the Controlled Inputs update. The big rewrite will now be v6
, but references to it below talk about v5
.
The initial alpha version of this proposal has been released:
Greetings all,
There are a number of feature requests, bugs, and performance issues that have caused me to rethink the design of redux-form
. I'd like to use this issue as a way to introduce you to some of my thinking, prepare you for what's to come, and get your feedback. Hopefully at least one of the features will make you excited to migrate to v5
when it comes out.
redux-form
v5
API Proposal
Glossary
connected: subscribed to changes in the Redux store with connect()
1. The form should not be connected
When writing the initial API, I provided things that seemed convenient for small forms, like having this.props.values
be the current state of the values of your form. This required having the wrapping form component to be connected to the Redux store, _and to re-render on every value change_. This doesn't matter if it's just a username
and password
form, but some of you are writing complex CRM systems with potentially hundreds of fields on the page at a time, causing redux-form
to grind to a halt. #529
So, what does this mean?
- The convenience functionality of providing
mapStateToProps
and mapDispatchToProps
is going away. You'll have to use connect()
directly yourself.
- But if the form isn't connected, then...
2. Each field is connected
Each field should subscribe only to the very specific slice of the Redux state that it needs to know how to render itself. This means that redux-form
can no longer only provide you with props to give to your own inputs.
import React, { Component } from 'react'
import { reduxForm } from 'redux-form'
@reduxForm({
form: 'login',
fields: [ 'username', 'password' ]
})
export default class LoginForm extends Component {
render() {
const { fields: { username, password } } = this.props
return (
<form>
<input type="text" {...username}/> // <--- can no longer use this syntax
<input type="password" {...password}/> // <--- can no longer use this syntax
</form>
)
}
}
So what do we do? The destructured props was one of the most elegant things about redux-form
!
There are three options that I want to support:
2.1. On-Demand Field Structure
The redux-form
user base seems pretty divided on how much they like providing the list of fields for the entire form, so I want to provide away to arbitrarily specify the path to your field at render-time. This can be done with a <Field/>
component that is given the name of the field in dot.and[1].bracket
syntax.
import React, { Component } from 'react'
import { reduxForm, Field } from 'redux-form'
@reduxForm({
form: 'userInfo' // <----- LOOK! No fields!
})
export default class UserInfoForm extends Component {
render() {
return (
<form>
<Field
name="name" // <---- path to this field in the form structure
component={React.DOM.input} // <---- type of component to render
type="text"/> // <--- any additional props to pass on to above component
<Field name="age" component={React.DOM.input} type="number"/>
<Field name="preferences.favoriteColor" component={React.DOM.select}>
<option value="#ff0000">Red</option> // <--- children as if it were a <select>
<option value="#00ff00">Green</option>
<option value="#0000ff">Blue</option>
</Field>
</form>
)
}
}
2.1.1. Custom Input Components
Another key feature of redux-form
is the ability to very simply attach custom input components. Let's imagine a hypothetical input component called SpecialGeolocation
that requires the following props:
location
- the location to display, in a colon-delimited 'lat:long'
string
onUpdateLocation
- a callback that is given the new 'lat:long'
string
Let's further complicate the matter by requiring that we save our location in a { latitude, longitude }
json object. How would we use this component in redux-form
v4.x
and v5
?
Well, given the extra string-to-json requirement, we need a complimentary pair of format
and parse
functions. We need these in _both_ v4.x
and v5
!
const format = jsonLocation => `${jsonLocation.latitude}:${jsonLocation.longitude}`
const parse = colonDelimitedLocation => {
const tokens = colonDelimitedLocation.split(':')
return { latitude: tokens[0], longitude: tokens[1] }
}
In v4.x
we map the props manually...
// v4.x
const { fields: { myLocation } } = this.props;
// ...
<SpecialLocation
location={format(myLocation.value)}
onUpdateLocation={value => myLocation.onChange(parse(value))}/>
...and in v5
we have to specify a mapProps
function to do it.
// v5
<Field
name="myLocation"
component={SpecialLocation}
mapProps={props => ({
location: format(props.value),
onUpdateLocation: value => props.onChange(parse(value))
})}/>
Simple enough, don't you think?
2.2. Provided field structure
The benefit of providing your fields array as a config parameter or prop to your decorated component is that redux-form
, as it does in v4.x
, can provide you with the connected field components as props. So, whereas the this.props.fields
provided to v4.x
were the props to your input components (value
, onChange
, etc.), in v5
they are now the Field
components (see 2.1 above) which will apply those props to an input component of your choosing.
import React, { Component } from 'react'
import { reduxForm } from 'redux-form'
@reduxForm({
form: 'userInfo',
fields: [ 'name', 'age', 'preferences.favoriteColor' ]
})
export default class UserInfoForm extends Component {
render() {
const { fields: { name, age, preferences } } = this.props
return (
<form>
<name component={React.DOM.input} type="text"/>
<age component={React.DOM.input} type="number"/>
<preferences.favoriteColor component={React.DOM.select}>
<option value="#ff0000">Red</option>
<option value="#00ff00">Green</option>
<option value="#0000ff">Blue</option>
</preferences.favoriteColor>
</form>
)
}
}
2.2.1. Custom Components
You can do the same thing as described in 2.1.1 above with these fields.
<myLocation
component={SpecialLocation}
mapProps={props => ({
location: format(props.value),
onUpdateLocation: value => props.onChange(parse(value))
})}/>
2.3. Convenience Inputs
All that component={React.DOM.whatever}
boilerplate is pretty ugly. In v5
, I want to provide convenience properties. These will be provided using memoized getters, so they will only be constructed if you use them. The use of getters means saying a not-so-fond farewell to IE8 support. React is dropping its support, and this library will as well.
Look how concise and elegant this is:
import React, { Component } from 'react'
import { reduxForm } from 'redux-form'
@reduxForm({
form: 'userInfo',
fields: [ 'name', 'age', 'preferences.favoriteColor' ]
})
export default class UserInfoForm extends Component {
render() {
const { fields: { name, age, preferences } } = this.props
return (
<form>
<name.text/> // <input type="text"/>
<age.number/> // <input type="number"/>
<preferences.favoriteColor.select>
<option value="#ff0000">Red</option>
<option value="#00ff00">Green</option>
<option value="#0000ff">Blue</option>
</preferences.favoriteColor.select>
</form>
)
}
}
All of the types of <input/>
will be supported as well as <select>
and <textarea>
. These will be able to handle special quirks about how checkboxes or radio buttons interact with data.
2.3.1. Submit and Reset buttons
Just like the fields, this.props.submit
and this.props.reset
button components will also be made available, with flags for common functionality you might expect, e.g. disableWhenInvalid
. More on form submission below.
3. Displaying Errors
Also as a getter on the field objects, like in 2.3 above, there will be an error
component that will output a <div>
with the error message.
render() {
const { fields: { name, city, country } } = this.props
return (
<form>
<name.text/>
<name.error/> // will only show if there is an error and the "name" field is "touched"
<city.text/>
<city.error showUntouched/> // will always show if there is an error
<country.text/>
<country.hasError> // allows for larger structure around error
<div>
<strong>OMG, FIX THIS!</strong>
<country.error/>
</div>
</country.hasError>
</form>
)
}
4. Sync Validation
The move in v5
to decentralize the power from the outer form element to the fields themselves proves a problem for how redux-form
has historically done sync validation, as the form component cannot rerender with every value change.
4.1. Sync Validate Entire Record
Traditionally, your sync validation function has been given the entire record. I would still like to have a "validate this entire record" functionality, but it is going to have to move to the reducer, and the reducer will store the sync validation errors in the Redux state.
4.2. Sync Validate Individual Fields
Many redux-form
users over its lifetime have requested the ability to put required
or maxLength
validation props directly on the input components. Now the redux-form
is controlling the field components, it makes perfect sense to do this. v5
will attempt to use the same props as defined in the HTML5 Validity spec, as well as to set the errors directly onto the DOM nodes with setCustomValidity()
as defined by said spec. #254.
5. Async Validation
For async validation, you will have to specify which fields will need to be sent to the async validation function so that redux-form
can create a non-rendering connected component to listen for when those fields change, similar to asyncBlurFields
works in v4.x
, except that the async validation function will only receive the fields specified, not the whole record.
5.1. Debounce
I'm not certain of the API yet, but some mechanism for allowing async validation to fire on change (after a set delay), not just on blur, will be provided. I'm open to suggestions.
6. Normalizing
Since sync validation is moving to the field components, I think it makes sense for normalizers to be there as well. Something like:
<username.text normalize={value => value.toUppercase()}/>
What I liked about normalizing being on the reducer in v4
was that your value would get normalized even if it was modified from some non-field dispatch of a CHANGE
action, but I don't think that was happening very often in practice. It can certainly be added back in the future if need be.
7. Formatters and Parsers
Formatters and parsers are the first cousins to normalizers. Say you want to display a phone number text field in the format (212) 555-4321
, but you want to store the data as just numbers, 2125554321
. You could write functions to do that.
<shipping.phone
format={value => prettyPhone(value)}
parse={value => value.replace(/[^0-9]/g, '')}/> // strip non numbers
The formatter is taking the raw data from the store and making it pretty for display in the input, and the parser takes the pretty input value and converts it back to the ugly store data.
8. ImmutableJS
Many of you have been holding your breath so far hoping I'd get to this. :smile:
I've gone back and forth and back and forth on this topic. I rewrote the whole reducer to use ImmutableJS, and the rewrote it in plain js objects several times. I'm certain of the following things:
- ImmutableJS has a very important role to play in Redux and making fast React applications
- It's not faster than plain objects for small structures
- A library like
redux-form
should NOT require that its users keep their Redux state as ImmutableJS objects
- Using ImmutableJS internally and giving the state back with toJS() is a terrible idea.
So, in conclusion, I'm officially forbidding the use of ImmutableJS with redux-form
. Deal with it.
:stuck_out_tongue_winking_eye: :stuck_out_tongue_winking_eye: Nah, I'm just kidding. I've actually solved this problem. :stuck_out_tongue_winking_eye: :stuck_out_tongue_winking_eye:
If you do this...
import { reduxForm } from 'redux-form'
...then redux-form
will keep all its internal state in plain javascript objects, doing shallow copies up and down the tree to not mutate the objects that don't need to change (be rerendered).
But, if you do this...
import { reduxForm } from 'redux-form/immutable'
...then redux-form
will keep _all its internal state_ in ImmutableJS objects.
I've managed to hide the internal state structure behind a minimal getIn
/setIn
façade enabling the same reducer code to work on both types. This has taken up most of my effort on v5
so far, but it's working beautifully with extensive tests.
9. Separation of Values and Form State
As a library user you shouldn't care too much about this, but many of the problems and bugs (#628, #629, #662) that v4.x
has suffered from have been a result of the internal state structure being like this:
// example state in v4.x
{
firstName: {
initial: 'Erik'
value: 'Eric',
touched: true,
submitError: 'Not the most awesome spelling'
},
lastName: {
initial: '',
value: 'Rasmussen',
touched: true,
submitError: 'Sounds too Scandinavian'
}
}
In v5
, the initial values, current values, and field state will be kept separate.
// example state in v5
{
initial: {
firstName: 'Erik',
lastName: ''
},
values: {
firstName: 'Eric',
lastName: 'Rasmussen'
},
fields: {
firstName: {
touched: true,
submitError: 'Not the most awesome spelling'
},
lastName: {
touched: true,
submitError: 'Sounds too Scandinavian'
}
}
}
This will make the RESET
action literally just do this:
return {
values: state.initial,
initial: state.initial
}
It will also enable things like subscribing to all the form values if one choses to. Such a decorator will be provided if only to enable the library demos.
10. Form Submission
"But!", you say, "If the form component isn't connected, how can it submit the values from the form component?"
This was a big problem with this inversion of control, but I have invented a way for a large outer component to be able to read values from the Redux store without being directly connected, and therefore rerendering on every value change. This should allow for a handleSubmit()
paradigm similar to v4.x
, but I haven't gotten to proving that yet.
11. What is going away?
I think the only functionality that going away is formKey
and reduxMountPoint
11.1. formKey
formKey
dates back to before you could specify form
as a prop (it used to only be a config parameter). It was designed to allow you to use the same form shape multiple times on a page, but you can do that with form
. The only thing stopping the removal of formKey
before was normalizers, but since they are moving to the fields (see 6 above), formKey
can be laid to rest.
11.2. reduxMountPoint
reduxMountPoint
was obsoleted by the more flexible getFormState()
, which v5
will still have.
12. Documentation, Examples, and Website
I'm not happy with how the examples are currently implemented on the redux-form
site. I like how the Redux examples are each in their own little folder that can be run individually. For v5
, I want to meet the following goals for the docs:
- Documentation exists in the
master
branch in a way that is navigable entirely on Github
- Examples in their own folder in the
master
branch in a way that can be run locally with npm install
and npm start
- That same documentation (literally the same markdown files) navigable on redux-form.com
- Those same examples running in separate little single-page apps on redux-form.com. They will be separate apps to demonstrate how some of them use third party libraries, like ImmutableJS or MaterialUI.
- Enable anchor linking to specific parts of the docs. This means the docs can't be a single-page app with React Router anymore, due to github.io forcing the use of
hashHistory
.
- redux-form.com hosted on github.io, which only hosts static files.
That is not an easy requirements list to hit, but I've figured out a way.
13. Roadmap
The following will not make it into the release of v5
, but v5
is being designed with these future features in mind.
13.1. Custom Library Extensions
In the same way that the standard inputs in React.DOM
will be provided as convenience properties (see 2.3 above), redux-form
v5
will (eventually) allow extensions to be applied via third party extension libraries to make it easy to use a third party input library. So imagine something like this:
import { reduxForm } from 'redux-form'
import reduxFormMaterialUi from 'redux-form-material-ui'
import MenuItem from 'material-ui/lib/menus/menu-item'
reduxForm.extend(reduxFormMaterialUi)
...
<form>
<name.muiTextField hintText="Hint text"/>
<meeting.start.muiTimePicker format="24hr"/>
<preferences.favoriteColor.muiSelectField>
<MenuItem value="#ff0000" primaryText="Red"/>
</preferences.favoriteColor.muiSelectField>
</form>
React Native is the most obvious extension, but any input library could be done. React Bootstrap, React Select, Belle, Date Pickers, Wysiwyg editors, etc. could all be done in this way.
One thing I'm not 100% sure about is whether or not extensions should be allowed to overwrite the built-in convenience components, like text
or password
, or if they should be prefixed as in my example above. Opinions welcome.
13.2. HTML5 Validity
It's possible that the actual setCustomValidation()
stuff mentioned in 4.1 above will not make the initial release.
Conclusion
As you can see, I've been putting some thought into this. Thank you for taking the time to read through it all. If you see any glaring mistakes or think something should be done differently, I am very open to constructive criticism. I'm pretty excited about these changes, and I hope you are, too.
discussion