React-redux-form: Is it possible to have a workflow like in redux-form

Created on 3 Jun 2017  路  11Comments  路  Source: davidkpiano/react-redux-form

The Problem

Hey.

I tried RRF before, but I like the RF workflow and design decisions more. However, every time I pick RF, I find bugs. Again I picked it and after half an hour, I found a bug with validation. Like really, wtf, I have a simple form with 3 input fields and already something is not working correctly.

Filed an issue - still no answer. The activity in RF commits is bad and the issue number is big. In RRF the situation is the opposite - high activity, few issues.

In RRF I still don't understand why can't it be that simple:

const rules = { ... }
const validate = (rules, values) => { return true }

<Form
  name="userForm"
  initialState={{ username: 'Mike' }}
  validate={values => validate(rules, values)}
  onSubmit={this.handleSubmit}
>
...
</Form>

The situation with React forms is so sad. A year passed and nothing has changed. I really don't want to use my own custom forms when there are big popular libraries.

So the only thing I wanted to ask is if it's possible in the next versions (maybe in 2.0) to make it work more like RF works (or make it optional)? I really like the activity and responsiveness in RRF.

enhancement question

Most helpful comment

Okay, let's see how we can improve or utilize the existing API to make things easier, like you said.

In RF you setup store once and never come here again

Try this:

const store = createStore(combineForms({
  forms: {}
});

That's all you need to do. Want to create a dynamic form? It'll happen automatically, e.g., if you do something like:

dispatch(actions.change('forms.surpriseNewForm.whatever', true));

// will create:
{
  forms: {
    $form: { <form state> },
    surpriseNewForm: {
      $form: { <form state> },
      whatever: { <field state> value: true }
    }
  }
}

Since I very rarely have wizard forms, I always wanted to clean my store in RRF.

Fair enough. Would adding a prop make this easier?

<Form
  model="user"
  transient={true}
>

When transient === true, on componentWillUnmount, an actions.reset action can be dispatched automatically. If you want, I can add this prop. Would take me 5 minutes.

In RF you use HOC and this props are passed into your form which I find very convenient.

It's convenient but way too magical, and with:

  • potential unwanted prop overriding
  • your _entire form_ will update on every single change. Bad for performance.

But if you wanted to, since _it's just Redux_, you can just have something like this:

class UserForm extends Component {
  // ...
}

const mapStateToProps = ({ forms }) => ({
  form: forms.user.$form
});

export default connect(mapStateToProps)(UserForm);

That's very unmagical, but does the exact same thing (under the form namespace) and provides the entire form state to you.

Would you want me to add a helper decorator, such as @withForm('user')?

All 11 comments

The situation with React forms is so sad.

Cheer up! All you have to do is suggest features and make PRs.

So the only thing I wanted to ask is if it's possible in the next versions (maybe in 2.0) to make it work more like RF works (or make it optional)? I really like the activity and responsiveness in RRF.

Can you make a list of specific features that you would like to see?

Also, funny enough, if you use <LocalForm>, you can get a form like you have above working with RRF, and with a bonus: you don't even have to set up Redux. At all.

// there's better ways to do this; I don't like how
// Redux-Form uses a single function. That means you have to recalculate
// every single field's validity on every single change.
const validate = values => {
  const nameValid = values.name && values.name.length > 0;
  const emailValid = values.email && isValidEmail(values.email);

  return { name: nameValid, email: emailValid };
};

const handleSubmit = values => console.log(values);

// in render
<LocalForm
  initialState={{ name: '', email: '' }}
  validators={validate}
  onSubmit={handleSubmit}
>
  <Control.text model=".name" />
  <Control.text type="email" model=".email" />
  <button>Submit!</button>
</LocalForm>

You can't get much simpler than that. 馃槈

Cheer up! All you have to do is suggest features and make PRs.

It works only if the maintainer is active, but when your issues (with bug repo, bug codepens) are left with no answers you don't even know what to do. That's what I absolutely love in RRF 鉂わ笍

Also, funny enough, if you use <LocalForm>

Yeah, I can probably try it as I don't need to share form's state. I love Redux though :)

What I liked in RF:

  • setup store once. No need to return to it when a new form is added.
  • no initial values in store creation. Same as above, setup store once, never come back when form is added / initial values are changed
  • forms are not persisted in store. Created when form is mounted, removed when form is unmounted (lazy flag?)

As for validation, I use ajv to validate json-schema both on client and server. So it looks like this:

const schema = {
  type: 'object',
  required: ['url', 'seoTitle', 'seoDescription'],
  properties: {
    url: { type: 'string', minLength: 2, maxLength: 20 },
    seoTitle: { type: 'string' },
    seoDescription: { type: 'string' }
  }
}

It is passed to validate function and is validated all together. It's convenient to have the same validation on client/server and I don't think it makes a big difference in speed vs single field validation except maybe if the form is huge.

I would love to give LocalForm a try as I'm sick of RF bugs, inactivity and will hope that I won't need its state somewhere else.

@davidkpiano I checked LocalForm and I'm not sure if it's a good replacement because:

  • all LocalForm's create its own redux store under the hood (not sure if this is desired when you already have global redux store)
  • how do you get form props, like submitting prop?

setup store once. No need to return to it when a new form is added.

Can you elaborate on this? You only need a one-time setup with RRF too.

no initial values in store creation. Same as above, setup store once, never come back when form is added / initial values are changed

Same thing, you can create forms dynamically without needing to change the structure of the store.

forms are not persisted in store. Created when form is mounted, removed when form is unmounted (lazy flag?)

That's what <LocalForm> is for - non-persistence. Forms persisting in the store was an intentional design decision - this is a library for making complex forms easily, which includes multi-step wizard forms.

As I showed above, you _can_ use a single function, just like in RF, to do your validation. It's just not recommended (yes, there are big performance concerns).

how do you get form props, like submitting prop?

Use onUpdate, which gives you the entire form state (including the pending prop for when a form is submitting), and do whatever you want with that state (save it to component state, etc. http://davidkpiano.github.io/react-redux-form/docs/guides/local.html

Can you elaborate on this? You only need a one-time setup with RRF too.

What I mean is from the docs:

const initialUserState = {};
const initialGoatState = {};

const store = createStore(combineForms({
  user: initialUserState,
  goat: initialGoatState,
}));

Maybe I misunderstood, but every time you have a new form (user: initialUserState for e.g.) or your form's initial state (initialUserState for e.g.) has to be changed - you come here and add or update this.

In RF you setup store once and never come here again:

import { reducer as formReducer } from 'redux-form'
const store = createStore(combineReducers({ ..., form: formReducer }))

Forms persisting in the store was an intentional design decision

Yeah, that's what I liked in RF. Forms are deleted from the store and if you need a wizard form (do you need them more than regular forms?) you add destroyOnUnmount: false prop. Since I very rarely have wizard forms, I always wanted to clean my store in RRF.

Use onUpdate to get props

This is why LocalForm is inconvenient, you still need to setup a wrapper form, define the whole form state, onUpdate, onChange functions. It's great when you don't have redux, but in the long run Form is more suitable. How can you get it in Form component - I guess you connect to redux store to get this props?

In RF you use HOC and this props are passed into your form which I find very convenient.

So, as I said in the topic start - this are all design decisions I really like in RF. Minimum setup and more suitable in my use-cases, but there are some bugs and inactivity.

Okay, let's see how we can improve or utilize the existing API to make things easier, like you said.

In RF you setup store once and never come here again

Try this:

const store = createStore(combineForms({
  forms: {}
});

That's all you need to do. Want to create a dynamic form? It'll happen automatically, e.g., if you do something like:

dispatch(actions.change('forms.surpriseNewForm.whatever', true));

// will create:
{
  forms: {
    $form: { <form state> },
    surpriseNewForm: {
      $form: { <form state> },
      whatever: { <field state> value: true }
    }
  }
}

Since I very rarely have wizard forms, I always wanted to clean my store in RRF.

Fair enough. Would adding a prop make this easier?

<Form
  model="user"
  transient={true}
>

When transient === true, on componentWillUnmount, an actions.reset action can be dispatched automatically. If you want, I can add this prop. Would take me 5 minutes.

In RF you use HOC and this props are passed into your form which I find very convenient.

It's convenient but way too magical, and with:

  • potential unwanted prop overriding
  • your _entire form_ will update on every single change. Bad for performance.

But if you wanted to, since _it's just Redux_, you can just have something like this:

class UserForm extends Component {
  // ...
}

const mapStateToProps = ({ forms }) => ({
  form: forms.user.$form
});

export default connect(mapStateToProps)(UserForm);

That's very unmagical, but does the exact same thing (under the form namespace) and provides the entire form state to you.

Would you want me to add a helper decorator, such as @withForm('user')?

Here is my simple perfect workflow:


- user form

const UserForm = ({ buttonText, submitting, handleSubmit }) => (
  <form onSubmit={handleSubmit}>
    <Input name="username" label="Username" />
    <Input name="password" type="password" label="Password" />
    <ButtonGroup>
      <Button type="success" loading={submitting} disabled={submitting}>{buttonText}</Button>
      <Link to="/users">Cancel</Link>
    </ButtonGroup>
  </form>
)

export default form({
  name: 'userForm',
  initialValues: {
    username: 'Mike' <-- just as initial value example
  },
  validate: values => validate(schema, values)
})(UserForm)


- user create page

class UserCreate extends Component {
  handleSubmit = values => {
    return doSmth(values)
  }

  render() {
    return (
      <section>
        <p>create</p>
        <UserForm onSubmit={this.handleSubmit} />
      </section>
    )
  }
}


- user update page

class UserEdit extends Component {
  state = {
    isLoading: true,
    error: null,
    item: null
  }

  componentDidMount() {
    this.fetchUser()
  }

  fetchUser() {
    // fetch user and update state
  }

  handleSubmit = values => {
    return doSmth(values)
  }

  render() {
    const { isLoading, error, item } = this.state

    if (isLoading) {
      return <p>loading</p>
    }

    if (!isLoading && error) {
      return <p>error</p>
    }

    if (!isLoading && !item) {
      return <p>user doesn't exist</p>
    }

    return (
      <section>
        <p>update</p>
        <UserForm initialValues={item} onSubmit={this.handleSubmit} />
      </section>
    )
  }
}


1) So, I create dynamic forms store:

const store = createStore(combineForms({
  forms: {}
})

2) Now I need to define UserForm component. Simple component vs HOC (decorator)?

  • If this is a simple Form component, I won't be able to use form state without connecting to redux store, so you will still use connect decorator.
  • withForm decorator can pass this values automatically.

I know, in RF it's like magic and you expect it to re-render on every change, but there are a lot of lifecycle checks in RF's HOC and it's optimised. I also expected it to re-render on every input change, but no. I do think it complicates things though.

I'd prefer HOC as I need form props and will still need to use connect in first variant.

3) Create page is easy, but where do you specify initialValues? In RF you do this in form HOC. How to do this in RRF, dispatch?

4) Update page has a fetch function, when user is received I pass it as initialValues into the form, HOC receives it and uses. How to do this in RRF, dispatch?

What do you think about this workflow, possible with RRF? Correct me or suggest better ideas, you know more form use-cases, that's just what I used to do in RF.

Not having a straightforward way to set initialValues in the form component rather than in the Redux store is the only reason I haven't switched over to RRF instead of RF at this point.

@icopp @VladShcherbin How does this future API look to you?

<Form
  model="user"
  initialState={{ firstName: 'Foo', lastName: 'Bar' }}
>
  // ... 
</Form>

Behind the scenes, this will call actions.load('user', {...}) to populate the initial state. I'm opting to call it initialState to:

  • maintain parity with <LocalForm initialState={{...}} >
  • keep consistency with Redux's notion of initial state, which is what this is.

Would this help you move to a better workflow? With this, you wouldn't have to specify initial state in Redux:

const store = createStore(combineForms({
  user: {}
}));

@davidkpiano yeah, this would definitely be a nice feature.

How do you combine the initial form state with initial loaded state from fetched item for example?

const initialValues = { isVisible: true }

const ArticleForm = ({ article }) => {
  const formState = { ...initialValues, ...article }

  return <Form model="articleForm" initialState={formState} />
}

Smth like this?

Well currently, you can just dispatch actions.load('articleForm', article) once the data is resolved.

Was this page helpful?
0 / 5 - 0 ratings