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

redux-form v6 API Proposal #726

Closed
erikras opened this issue Mar 10, 2016 · 101 comments
Closed

redux-form v6 API Proposal #726

erikras opened this issue Mar 10, 2016 · 101 comments
Milestone

Comments

@erikras
Copy link
Member

erikras commented Mar 10, 2016

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:

April 20, 2016 - v6.0.0-alpha.4 Release


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. 😄

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.


😜 😜 Nah, I'm just kidding. I've actually solved this problem. 😜 😜

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.

@tomduncalf
Copy link
Contributor

Wow, big changes! Thanks for putting such a detailed proposal together. I'll have to have a proper think about how this would impact the way I've been building forms with redux-form, and whether it would be feasible to migrate the current project I am working on to this API or if it's more likely I'd stick with v4 and use v5 in the next project.

The formatters thing is definitely a pretty compelling proposal, done this myself recently in a much less elegant way :) (although you might be talking about something subtly different, mine is formatting as you type e.g. inserting spaces in a credit card number, rather than formatting after you blur)

@tomduncalf
Copy link
Contributor

So one thing that I noticed is that I sometimes want to manually call the onChange handler for a field... for example, changing the value of a radio input based on clicking a button inside a custom input component like:

setOther() {
    this.props.inputProps.onChange('other')
}

...

<a onClick={this.setOther}>Other</a>

How would we go about this with the v5 API? Dispatching redux-form change actions instead?

@davidkpiano
Copy link

This is a pretty big deviation from the API, no? It's looking a lot more like React-Redux-Form. Much more similarities.

@erikras
Copy link
Member Author

erikras commented Mar 10, 2016

@tomduncalf Excellent point. The field objects (components) should have methods on them to dispatch actions. Perhaps the on can be dropped and it can just be called change(value) Another option is for the props (the object that v4.x provided) themselves be available, perhaps through a getter. These are details that I haven't nailed down yet because I haven't gotten quite to that part of it. Something like this:

const { fields: { firstName } } = this.props
return (
  <form>
    <firstName.text/>
    <button onClick={() => firstName.change('Tom')}>Set name to Tom</button>
  </form>
)

@davidkpiano Yes, your library was very inspiring. As was the entire discussion on #529.

@tomduncalf
Copy link
Contributor

The idea of having the v4 style props available is interesting, as it perhaps provides a way to migrate v4 to v5 without a total overhaul. I still need to process the new API proposal and how it would affect my code in real terms to be able to properly comment though!

Wondering if you have any "finger in the air" guesses at a rough timeline for these changes? (e.g. 1/3/6 months kind of level)

@erikras
Copy link
Member Author

erikras commented Mar 10, 2016

@tomduncalf Ooh, I like the "props available as migration step" idea. 👍

I'd say 1-3 months.

@erikras
Copy link
Member Author

erikras commented Mar 10, 2016

This is a pretty big deviation from the API, no?

@davidkpiano Yeah, it's pretty much all dictated as a result of point 1.

@diogobeda
Copy link

Great great work on the proposal @erikras! 🎉

I also have a pretty big app using v4 and will think about the way v5 affects my codebase. Then, I'll get back here with feedback.

@davidkpiano
Copy link

I will say that a lot of developers might have an aversion to the "convenience inputs" such as <name.age>. Just looking at the code, it wrongfully assumes that there exists a custom <name> element type that isn't defined anywhere. Might not be worth dropping IE8 support just for that.

Why not try something more succinct, like <Field control="input" /> ?

@erikras
Copy link
Member Author

erikras commented Mar 10, 2016

Why not try something more succinct, like <Field control="input" />

Yes, using strings is another very real possibility. Good feedback.

@davidkpiano
Copy link

In retrospect, for RRF, I would have done something like <Control type="radio" /> for single control components. <Field> is meant to contain one or more children, so that things like this are possible:

<Field model="user.choices[]">
  <input type="checkbox" value="foo" />
  <input type="checkbox" value="bar" />
  <input type="checkbox" value="baz" />
</Field>

@ooflorent
Copy link
Contributor

All these changes are really exciting and I'm grateful to you to give the community a chance to share its ideas.

I would like to share some details about how we use redux-form in order to help everyone understand our constraints and what shaped our thoughts.

We are using redux-form in relatively big React application. Currently its JavaScript codebase (excluding third-party packages, tests, scripts, ...) weights around 25.000 lines, 500 files, 200 components, 50 forms. It is growing fast and it may triple its size by the end of the year. Our forms are mostly composed of dynamic and nested fields of complex inputs fed with react-relay records. redux-form is a critical part of our application and are taking every little change seriously.

So, what can I say about this API proposal?


tl;dr Strings, meh! Objects, yeah!


Disclaimer: I hate JSX (and a lot of people do too).

1. The form should not be connected

I totally agree here. We had to create complex wrappers to deal with performance issues on large forms (somewhat what is proposed in 2.1. Moreover, I prefer composing connect and reduxForm (and other decorators). Each one has one job and must not overlap with other HoCs.

2.1. On-Demand Field Structure

This is great. Really. This is how we create forms in our application. But I'm not sure about the component property.

  • How to deal with Flowtype checks? It might be really tricky.
  • How do you add props to the components? Spreading Field's props into the input's one can create conflicts (e.g. what if my custom input need a component property?)

We tried to solve these issues using the children property of Field.

// JSX-friendly approach but requires code instrumentation using `context` or cloning children.
<Field name="picture">
  <ImageInput {...fields.picture} />
</Field>

// Less JSX-friendly approach.
// Instrument `field` and pass it down.
<Field name="picture">
  field => <ImageInput {...field} />
</Field>

// Fortunately it is awesome using component factories!
field({name: "picture"}, imageInput)
field({name: "picture"}, field => imageInput({...field, otherProp: "foo"}))

mapProps could do the job but it looks cumbersome in JSX.

This can be done with a <Field/> component that is given the name of the field in dot.and[1].bracket syntax.

This can be hard to manage if a form declares nested dynamic fields (I know, redux-form does not officially support it but it can achieve using addArrayValue).

In my opinion, the name property is a bad idea. But there is hope! The fields property may be shaped this way:

// Types
type FieldRef = {
  $$typeof: "ref"
  path: string
}

type FieldMap = {
  $$typeof: "map"
  [field: string]: FieldRef | FieldMap | FieldSet
}

type FieldSet = {
  $$typeof: "set"
  [index: number]: FieldRef | FieldMap
}

// Sample dump
{
  $$typeof: "map",
  name: {$$typeof: "ref", path: "name"},
  dot: {
    $$typeof: "map",
    and: {
      $$typeof: "set",
      0: {$$typeof: "ref", path: "dot.and[0]"},
      1: {
        $$typeof: "map", 
        "bracket": {$$typeof: "ref", path: "dot.and[1].bracket"},
      },
    }
  },
}

Doing this, every change does not trigger a render since fields is just a tree description of the form schema. Adding a field generates a new tree and triggers a render. Hopefully Field would have a pure=true option (like Redux) which would prevent over-rendering. Moverover, it would be possible to include metadata into each node.

2.2. Provided field structure

While it's a good idea for JSX codebase, it's a disaster for non-JSX ones. fields values would have to be used like this:

function UserInfoForm({fields: {name, age, preferences}}) {
  return (
    DOM.form(null,
      React.createElement(name, {component: "input", type: "text"}),
      React.createElement(age, {component: "input", type: "number"}),
      React.createElement(preferences.favoriteColor, {component: "select"}, 
        DOM.option({value: "#f00"}, "Red"),
        DOM.option({value: "#0f0"}, "Green"),
        DOM.option({value: "#00f"}, "Blue"),
      )
    )
  )
}

If fields returns a factory, then it's great, but no longer work for JSX...

function UserInfoForm({fields}) {
  return (
    DOM.form(null,
      fields.name({component: "input", type: "text"}),
      fields.age({component: "input", type: "number"}),
      fields.preferences.favoriteColor({component: "select"}, 
        DOM.option({value: "#f00"}, "Red"),
        DOM.option({value: "#0f0"}, "Green"),
        DOM.option({value: "#00f"}, "Blue"),
      )
    )
  )
}

Please notice that I added quotes around component values because the ones you specified are factories. But supporting factories inside component would be a huge win!

2.3. Convenience Inputs

3. Displaying Errors

While I must admit this may be useful, in large codebase this is pretty useless, furthermore if you need to style components and always specify new props.

Think about what problems does redux-form try to solve? In my opinion, redux-form should keep its surface area as small as possible. It must manage a form and expose a convenient API to interact with. react should be a companion, a possible view layer.

4. Sync Validation

5. Async Validation

In Redux philosophy, reducers only apply mutations. Validation should belong to the middleware. If you treat sync and async validation the same way using VALIDATION_START, VALIDATION_SUCCESS, VALIDATION_FAILURE, it would be possible to hook on those actions in redux-saga.

5.1. Debounce

It belongs to user-land in my opinion.

6. Normalizing

Moving this to form-level or field level would be a great move. Having to declare normalizers outside of the form makes code-splitting (and chunk loading) a pain to deal with.

7. Formatters and Parsers

8. ImmutableJS

This is awesome 👍

9. Separation of Values and Form State

Currently it's impossible to add errors to a collection of fields. We can add an error to foo[1] but not to the entire foo field collection. We should find an internal representation which allow this.

10. Form Submission

Be careful, you cannot hold refs on functional components. Quote React documentation:

Refs may not be attached to a stateless function, because the component does not have a backing instance. You can always wrap a stateless component in a standard composite component and attach a ref to the composite component.

@erikras
Copy link
Member Author

erikras commented Mar 10, 2016

@ooflorent Wow! Excellent analysis. Thank you for pushing back on some API overreach, e.g. debounce. This is exactly the sort of well-reasoned feedback that I was hoping for.

Think about what problems does redux-form try to solve? In my opinion, redux-form should keep its surface area as small as possible. It must manage a form and expose a convenient API to interact with.

You may be right that I may be stepping too far into the realm of "convenience" and maybe redux-form should be less opinionated about how it is used. Finding this balance is tough. The myriad ways that the current handleSubmit() works shows some indecisiveness and causes much confusion for beginners. Just saying, "You've got to provide your submit function from outside your form with onSubmit" and taking away the other options might make for a cleaner API, for example.

It never occurred to me that people might be using redux-form and not using JSX. I can't say that I entirely understood your suggestion in section 2.1.


Was

tl;dr Strings, meh! Objects, yeah!
a reaction to my
Yes, using strings is another very real possibility. Good feedback.
?

It's certainly easy enough to

import { DOM: { input, select } } from 'react'

Thanks again for contributing to this discussion. Everything is still a work in progress.

@ooflorent
Copy link
Contributor

@erikras My tl;dr was referring to name="foo.bar[0].baz". I think we must avoid this at all costs.

@davidkpiano
Copy link

@ooflorent Why? It's basically the only way Redux-Form can know where to both get and set the value. Think of it as a simpler implementation of a Lens in functional programming.

@ooflorent
Copy link
Contributor

@davidkpiano I don't see how the first example is cleaner than the second:

/* 1 */ <Field name="foo.bar[1].baz" />
/* 2 */ <Field name={foo.bar[1].baz} />
  1. if I misspelled a field, redux-form would throw (or return undefined)
  2. if I misspelled a field, JavaScript will throw a TypeError, Flow may notice the problem

@davidkpiano
Copy link

Missing the point. You're sending a nonsense value to the name prop that in no way tells Redux Form that you're talking specifically about foo.bar[1].baz.

@ooflorent
Copy link
Contributor

If fields is implemented as described in my 2.1. comment then Redux Form can easily identify the field. Avoid strings in user-land, not internally.

@erikras
Copy link
Member Author

erikras commented Mar 10, 2016

My tl;dr was referring to name="foo.bar[0].baz". I think we must avoid this at all costs.

@ooflorent You're both talking past each other. I think what @ooflorent is suggesting is with the fields array provided.

const { fields: { foo } } = this.props
return (
    <form>
      <Field {...foo.bar[1].baz} component={input} type="text"/>  // either this
      <Field config={foo.bar[1].baz} component={input} type="text"/>  // or this
    </form>
  )

?

@ooflorent
Copy link
Contributor

@davidkpiano The FieldRef can include a path or a cursor referencing the internal field object. The second option is harder to implement but would result in performance wins.

@arolson101
Copy link

This may be an ignorant suggestion, but what about wrapping the return value of render to wrap the fields for you?

e.g.

render() {
  const { fields: { foo } } = this.props;
  return reduxFormDoMagic(
    <form>
      <input type="text" {...foo}/>
    </form>
  );
}

the spread operation {...foo} would add on marker properties and reduxFormDoMagic would recurse the tree and wrap the nodes so marked. Then the only migration people would have to do is wrap the return value, which could possibly be made easier with a decorator:

@reduxFormMagic
render() { ... }

@ralphschindler
Copy link

@erikras can you expand on section 4. Sync Validation usage scenarios?

Currently, I don't see any way to have both a sync validate function and custom submit function exist as component instance methods. The motivation is so that validate and submit both have access to component instance props via the same means. Ideally, I would like to be able to:

export default class MyForm extends Component {
  validate (values) {
    // use this.props
  }
  submit (values, dispatch) {
    // use this.props
  }
  render () {
    const { handleSubmit } = this.props;
    return (<form onSubmit={handleSubmit(::this.submit)}>...</form>);
  }
}

Currently validate has to exist outside the component, and even though it is passed props, it would be nice if custom validate() and custom submit() could be provided in a consistent manner. If my expected usage scenario is misguided, please let me know.

@johanneslumpe
Copy link
Contributor

@erikras Wow those are quite significant changes. I've recently run into issues regarding the performance where the whole form re-rendered, so I like the idea of having the fields be connected instead of the form container.

You've already mentioned this, but for me a very important part is that components like react-select continue to work seamlessly with the library. Furthermore I'd like us to figure out an easy upgrade path, maybe something like @tomduncalf suggested. Even if it won't be that, migrating from v4 to v5 should ideally be as easy was migrating from react-router 1.0 to 2.0.

One thing I didn't see in the spec above is how you want to handle submitting a form and storing the returned errors. Right now we can tunnel the errors using a rejected promise. Are you planning something similar for v5? If not, how are you planning to handle this?

@erikras
Copy link
Member Author

erikras commented Mar 10, 2016

@ralphschindler Another option, if we wanted to keep the "validate whole record" approach to sync validation would be to have a non-rendering component that could be popped optionally into the form to specify a validation function. That component would be connected to the form values, so it would be rerendered (though result in no output) on every value change. Something like:

import {reduxForm, SyncValidation} from 'redux-form'

@reduxForm({ ...config })
export default class MyForm extends Component {
  validate (values) {
    // use this.props
  }
  submit (values, dispatch) {
    // use this.props
  }
  render () {
    const { handleSubmit } = this.props;
    return (
        <form onSubmit={handleSubmit(::this.submit)}>
          ... // inputs
          <SyncValidation validate={::this.validate}/>
        </form>
      );
  }
}

Is that disgusting?


@johanneslumpe Yes, upgrade path is important. We have to figure out where we want to go first, and then we'll figure out how to get there. 😄 Because of the significance of this change, every input will have to be wrapped in a Field component, so it's going to be a pretty tedious upgrade no matter what.

@johanneslumpe
Copy link
Contributor

@erikras I agree. I' not sure, but maybe we could have a codemod for that? Although I believe that it might be huge effort to write one which fits all cases and doesn't arbitrarily break code.

Still, tedious !== hard. So the more straightforward we can make it the better :)

@ralphschindler
Copy link

👍 I like that option. My primary motivation is having a consistent extension story, and using this.props is a requirement since many times <select><option> values are provided through props from other reducers in the store (forms that have one-to-many, etc, relationships).

That said, I also like the concept of per-field, at-submit-time validation, and would be interested in you providing usage scenarios for that under 4.2 to better follow along and provide example to build off of (I am not sure I follow the request in #254, but I do like the idea of using HTML5 validation).

@tomduncalf
Copy link
Contributor

Re: per-field, at-submit-time validation - anything that prevented validating the contents of the form as a whole would be a no-go for me, as | am using yup (https://github.com/jquense/yup) to define a validation schema for each form, and also have fields for which the validation depends on other fields... so I would need a way to access the entire form values in the validation

@erikras
Copy link
Member Author

erikras commented Mar 10, 2016

@tomduncalf Right, you're the one doing client-side async validation on submit, which isn't supported very well at the moment. That should be an additional requirement.

@Plummat
Copy link

Plummat commented Apr 11, 2016

Not to (again) echo an earlier comment, but is there any consideration between keeping the current api as "redux-form", and releasing this new proposal as a separate package, perhaps titled "redux-complex-form"?

@avocadowastaken
Copy link
Contributor

Not to (again) echo an earlier comment, but is there any consideration between keeping the current api as "redux-form", and releasing this new proposal as a separate package, perhaps titled "redux-complex-form"?

Agree, let's make small steps (like React does) to achieve our goals without breaking lots of code,
Also keep all changes separately as addons

const reducer =  combineReducers({
    form: formReducer({
        addons: {
            normalize: require('redux-form-normalize'),
            components: require('redux-form-components'),
            'format-parse': require('redux-form-format-parse')
        }
    })
});

// -----

@reduxForm({
    addons: [
          'format-parse', // will add format and parse methods to every form field
          'normalize',  // will add normalize method to every form field
          'components', // will add new fancy component creators to props,
          (state, action) => {
              if (action.type === 'CHANGE') {
                 // ... do something
              }
              return state;
          }
    ]
})

So basically after every action (CHANGE, BLUR) form reducer will go through addon list of the form and modify store state with this addons, where every addons is also a reducer, can be registered in formReducer and can be custom

@erikthedeveloper
Copy link
Contributor

This issue/discussion is amazing. I can't believe I'm only now stumbling onto it. I'll look forward to seeing how I can contribute to v6.x+! Thank you [again] @erikras for all your hard work on this. It is greatly appreciated!

@erikras
Copy link
Member Author

erikras commented Apr 20, 2016

Hey guys. Just wanted to let you know v6.0.0-alpha has been released!

It doesn't contain all of the things in this proposal (many of them were not technically possible), but it gets a lot of them.

@anyong
Copy link

anyong commented Apr 20, 2016

Amazing! Been waiting for this because I am just getting into a new project, so I will be taking a look first thing tomorrow to start giving you feedback.

Thanks @erikras.

@morgante
Copy link

@erikras Thanks for pointing out the release.

I'm looking through the examples, particularly the synchronous validation example and I still can't see how form-wide elements are able to subscribe to form-wide errors. For example, I want my submit button to be disabled unless synchronous validation is passed.

Am I missing how to do it with v6, or is it still being worked on?

@erikras
Copy link
Member Author

erikras commented Apr 20, 2016

@morgante You still have many of the same props as earlier versions in your form component, including error (the form-wide error), as well as pristine / dirty and valid / invalid.
http://redux-form.com/6.0.0-alpha.4/docs/api/Props.md/

@AubreyHewes
Copy link

@erikras could you possibly rename this to something like redux-form v5 (née v6) API Proposal ? .. this may help explaining issues to mgmt that reference v5 though will be in v6 ..;-) Also a possible update to the original comment with the 6-alpha ? Thanks! You are doing a sterling job!
i.e. #629

@erikras erikras changed the title redux-form v5 API Proposal redux-form v6 API Proposal Apr 20, 2016
@erikras
Copy link
Member Author

erikras commented Apr 20, 2016

@AubreyHewes Yeah, React 15 really threw a kink in my release pipeline. Is this better?

@AubreyHewes
Copy link

@Erikas cheers, works for me ;-)

@avocadowastaken
Copy link
Contributor

@erikras great! but the part with sync validation made me sad,
I have application with lazy loading, and initializing validators in app root when forms didn't even loaded - is bad thing for me.

Is it possible to dynamically load validators to reducer?

// AppFormReducer.js

import { reducer } from 'redux-form'; 

export const formReducer = reducer.lazyReducer();

// App.js

import { combineReducers } from 'redux'
import { formReducer } from './AppFormReducer'

const reducer = combineReducers({
  form: AppFormReducer
});

// ./components/MyForm.js

import { reduxForm } from 'redux-form';
import { formReducer } from '../AppFormReducer' 

formReducer.registerValidator('myForm', (values, props) => {
    // do validations 
    return errors;
});

reduxForm({
  name: 'myForm'
})(MyForm)

@andrewmclagan
Copy link

@erikras we want to give you a little financial support for putting so much effort into this project... how do we do that.

@erikras
Copy link
Member Author

erikras commented Apr 21, 2016

@andrewmclagan Thanks. The easiest way is with the "donate" link (it's disguised as one of the badges) at the top of the README.

@erikras
Copy link
Member Author

erikras commented Apr 21, 2016

@umidbekkarimov Okay. There is a way to support the old way of passing in validation, but it requires refreshing the entire form when any sync validation error changes, so I made the decision to not provide that, but I will reconsider allowing that. It can be hard to strike a balance between being feature-full and accommodating everyone's needs and trying to keep a small API surface area for clarity. Thank you for your comment.

@anyong
Copy link

anyong commented Apr 21, 2016

@erikras: This seems like a bug. If you manually set an error object with stopSubmit action creator, and try to submit the form again without changing any of the fields, handleSubmit.js:38 throws an error:

Uncaught TypeError: submitFailed is not a function

(submitFailed === true for me)

@avocadowastaken
Copy link
Contributor

@erikras Thank You! it was the killer feature why I chose redux-form (we have large forms in projects where validation of one field depends on value of the other fields), all other problems with slow rendering - are react problems and can be solved by react itself (split form to smaller components and optimize with shouldComponentUpdate)

@leesiongchan
Copy link
Contributor

Thank you @erikras for this enormous rewrite of redux-form, it works so good so far, but what is missing here is the Array Fields. Do you have any release date on this particular feature?

@erikras
Copy link
Member Author

erikras commented Apr 26, 2016

Do you have any release date on this particular feature?

ASAP. Maybe a week or two? Kind of hard to block out man hours for OSS work.

@ooflorent ooflorent modified the milestone: next-6.0.0 Apr 28, 2016
@andrewmclagan
Copy link

Yes, agree so very very appreciative of your efforts. Hopefully our team wil have time in the near future to contribute

@franleplant
Copy link

Excelent API changes, very happy with them, good job

@lock
Copy link

lock bot commented Jun 3, 2018

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked as resolved and limited conversation to collaborators Jun 3, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests