66

I have been trying to figure out the best way to manage my react forms. I have tried to use the onChange to fire an action and update my redux store with my form data. I have also tried creating local state and when my form gets submitted I trigger and action and update the redux store.

How should i manage my controlled input state?

gkkirsch
  • 2,006
  • 2
  • 16
  • 14

5 Answers5

52

I like this answer from one of the Redux co-authors: https://github.com/reactjs/redux/issues/1287

Use React for ephemeral state that doesn't matter to the app globally and doesn't mutate in complex ways. For example, a toggle in some UI element, a form input state. Use Redux for state that matters globally or is mutated in complex ways. For example, cached users, or a post draft.

Sometimes you'll want to move from Redux state to React state (when storing something in Redux gets awkward) or the other way around (when more components need to have access to some state that used to be local).

The rule of thumb is: do whatever is less awkward.

That is, if you're sure that your form won't affect global state or need to be kept after your component is unmounted, then keep in the react state.

pgsandstrom
  • 14,361
  • 13
  • 70
  • 104
35
  1. You can use the component's own state. And then take that state and give it as an argument to the action. That's pretty much the "React way" as described in the React Docs.

  2. You can also check out Redux Form. It does basically what you described and links the form inputs with Redux State.

The first way basically implies that you're doing everything manually - maximum control and maximum boilerplate. The second way means that you're letting the higher order component do all the work for you. And then there is everything in between. There are multiple packages that I've seen that simplify a specific aspect of form management:

  1. React Forms - It provides a bunch of helper components to make form rendering and validation more simple.

  2. React JSON schema - Allows one to build an HTML form from a JSON schema.

  3. Formsy React - As the description says: "This extension to React JS aims to be that "sweet spot" between flexibility and reusability."

Update: seems these days Redux Form is being replaced with:

  1. React Final Form

And one more important contender in the space that's worth checking out is:

  1. Formik

Tried out React Hook Form in my last project - very simple, small footprint and just works:

  1. React Hook Form
Dmitry Shvedov
  • 3,169
  • 4
  • 39
  • 51
  • 3
    I think it depends on the situation. If other components need that state as well then you would probably want to hold it in redux. – Dylan Jan 22 '16 at 20:01
  • Hmm I generally disagree with this. I think you should only use component state for data that has to do with render/DOM logic (i.e. you need a toggle to show whether or not to display a component) and all application data should only use the Redux application state. However, as OP points out, this gets really tedious and repetetive with lots of inputs in a form. That's why I highly support using the Redux Form plugin you pointed out in your second paragraph. That pretty much solves the whole problem. – Andy Noelker Jan 22 '16 at 20:12
  • What I'm suggesting in the first paragraph is simply what's in the React Docs. Basically you're using the component state to store the values of your inputs, which are temporary until they are submitted, and at that point I wouldn't count them as application data. And yes, I prefer Redux Form. My only problem with it is that it makes your otherwise dumb components aware of Redux, which is not exactly Redux way. – Dmitry Shvedov Jan 22 '16 at 20:50
  • 4
    The other factor is the potential overhead with dispatching events on each keystroke, and causing all your other components to re-render. That can be optimized with use of `shouldComponentUpdate`, of course, but something you have to be aware of. Ultimately, there is no single absolute answer - find something that works for your app, and go with it. For what it's worth, I recently prototyped a wrapper component that handles current updates in its state, and triggers debounced actions to update the store ([visible in this gist](https://gist.github.com/markerikson/554cab15d83fd994dfab)) – markerikson Jan 23 '16 at 17:15
  • 2
    I went ahead and tried out Redux Form. It worked awesome. It uses all sorts of events onChange, onBlur, onFocus. It dispatches a lot of actions but seems to be pretty effective. Is there a definitive answer we can make out of this question or is this more of a debatable/situational thing? – gkkirsch Jan 25 '16 at 02:51
  • 1
    @gkkirsch Like with everything in Redux, it's your design decision. Frameworks like Angular dictate you to do things a certain way. Here, it's all about managing trade-offs. BTW, updated the answer with more options to consider. – Dmitry Shvedov Jan 25 '16 at 04:17
  • @Casimir it's a pretty old answer of mine, and I'm not sure I'd recommend redux-form having gone through everything that I did with it. Added couple of other options to the answer. I will personally try out Formik in my future project - as it promises to have less magic in it. – Dmitry Shvedov May 25 '18 at 18:54
  • @DmitryShvedov Just started using `redux-form` a week ago and so far it's been great. May I ask what kind of experiences you had with it that made you look elsewhere? – Janosh May 26 '18 at 08:09
  • 1
    @Casimir Problems start when your app becomes more complex - say you add multilanguage support to your forms, add some custom components, like color-pickers, add a prompt that prevents user from refreshing the page before saving, etc. I don't remember specific issues now, but overall - too much magic. Also it looks like the project is not very active now, so I don't think it's a good long-term decision to use it now. If you like it, switching to Final Form might make more sense - it's made by the same developer and has a similar API. – Dmitry Shvedov Jun 03 '18 at 23:16
8

TL;DR

It's fine to use whatever as it seems fit to your app (Source: Redux docs)


Some common rules of thumb for determing what kind of data should be put into Redux:

  • Do other parts of the application care about this data?
  • Do you need to be able to create further derived data based on this original data?
  • Is the same data being used to drive multiple components?
  • Is there value to you in being able to restore this state to a given point in time (ie, time travel debugging)?
  • Do you want to cache the data (ie, use what's in state if it's already there instead of re-requesting it)?

These questions can easily help you identify the approach that would be a better fit for your app. Here are my views and approaches I use in my apps (for forms):

Local state

  • Useful when my form has no relation to other components of the UI. Just capture data from input(s) and submits. I use this most of the time for simple forms.
  • I don't see much use case in time-travel debugging the input flow of my form (unless some other UI component is dependent on this).

Redux state

  • Useful when the form has to update some other UI component in my app (much like two-way binding).
  • I use this when my form input(s) causes some other components to render depending on what is being input by the user.
Divyanshu Maithani
  • 13,908
  • 2
  • 36
  • 47
5

Personally, I highly recommend keeping everything in the Redux state and going away from local component state. This is essentially because if you start looking at ui as a function of state you can do complete browserless testing and you can take advantage of keeping a reference of full state history (as in, what was in their inputs, what dialogs were open, etc, when a bug hit - not what was their state from the beginning of time) for the user for debugging purposes. Related tweet from the realm of clojure

edited to add: this is where we and our sister company are moving in terms of our production applications and how we handle redux/state/ui

-1

Using the helper libraries is just more quick and avoid us all the boilerplate. They may be optimized, feature rich ...etc. As they make all different aspects more of a breeze. Testing them and making your arsenal as knowing what's useful and better for the different needs, is just the thing to do.

But if you already implemented everything yourself. Going the controlled way. And for a reason you need redux. In one of my projects. I needed to maintain the form states. So if i go to another page and come back it will stay in the same state. You only need redux, if it's a mean for communicating the change to multiple components. Or if it's a mean to store the state, that you need to restore.

You need redux, if the state need to be global. Otherwise you don't need it. For that matter you can check this great article here for a deep dive.

One of the problems that you may encounter! When using the controlled inputs. Is that you may just dispatch the changements at every keystroke. And your form will just start freezing. It became a snail.

You should never directly dispatch and use the redux flux, at every changement. What you can do, is to have the inputs state stored on the component local state. And update using setState(). Once the state change, you set a timer with a delay. It get canceled at every keystroke. the last keystroke will be followed by the dispatching action after the specified delay. (a good delay may be 500ms).

(Know that setState, by default handle the multiple successive keystroke effectively. Otherwise we would have used the same technique as mentioned above (as we do in vanilla js) [but here we will rely on setState])

Here an example bellow:

onInputsChange(change, evt) {
    const { onInputsChange, roomId } = this.props;

    this.setState({
        ...this.state,
        inputs: {
            ...this.state.inputs,
            ...change
        }
    }, () => {
        // here how you implement the delay timer
        clearTimeout(this.onInputsChangeTimeoutHandler); // we clear at ever keystroke
              // this handler is declared in the constructor
        this.onInputsChangeTimeoutHandler = setTimeout(() => {
           // this will be executed only after the last keystroke (following the delay)
            if (typeof onInputsChange === "function")
                    onInputsChange(this.state.inputs, roomId, evt);
        }, 500);
    })
}

You can use the anti-pattern for initializing the component using the props as follow:

constructor(props) {
    super(props);

    const {
        name,
        description
    } = this.props;

    this.state = {
        inputs: {
            name,
            description
        }
    }

In the constructor or in the componentDidMount hook like bellow:

componentDidMount () {
    const {
        name, 
        description
    } = this.props;

    this.setState({
        ...this.state,
        inputs: {
            name,
            description
        }
    });
}

The later allow us to restore the state from the store, at every component mounting.

Also if you need to change the form from a parent component, you can expose a function to that parent. By setting for setInputs() method that is binded. And in the construction, you execute the props (that is a getter method) getSetInputs(). (A useful case is when you want to reset the forms at some conditions or states).

constructor(props) {
    super(props);
    const {
         getSetInputs
    } = this.props;

   // .....
   if (typeof getSetInputs === 'function') getSetInputs(this.setInputs);
}

To understand better what i did above, here how i'm updating the inputs:

// inputs change handlers
onNameChange(evt) {
    const { value } = evt.target;

    this.onInputsChange(
        {
            name: value
        },
        evt
    );
}

onDescriptionChange(evt) {
    const { value } = evt.target;

    this.onInputsChange(
        {
            description: value
        },
        evt
    );
}

/**
 * change = {
 *      name: value
 * }
 */
onInputsChange(change, evt) {
    const { onInputsChange, roomId } = this.props;

    this.setState({
        ...this.state,
        inputs: {
            ...this.state.inputs,
            ...change
        }
    }, () => {
        clearTimeout(this.onInputsChangeTimeoutHandler);
        this.onInputsChangeTimeoutHandler = setTimeout(() => {
            if (typeof onInputsChange === "function")
                onInputsChange(change, roomId, evt);
        }, 500);
    })
}

and here is my form:

 const {
        name='',
        description=''
 } = this.state.inputs;

// ....

<Form className="form">
    <Row form>
        <Col md={6}>
            <FormGroup>
                <Label>{t("Name")}</Label>
                <Input
                    type="text"
                    value={name}
                    disabled={state === "view"}
                    onChange={this.onNameChange}
                />
                {state !== "view" && (
                    <Fragment>
                        <FormFeedback
                            invalid={
                                errors.name
                                    ? "true"
                                    : "false"
                            }
                        >
                            {errors.name !== true
                                ? errors.name
                                : t(
                                        "You need to enter a no existing name"
                                    )}
                        </FormFeedback>
                        <FormText>
                            {t(
                                "Enter a unique name"
                            )}
                        </FormText>
                    </Fragment>
                )}
            </FormGroup>
        </Col>
        {/* <Col md={6}>
            <div className="image">Image go here (one day)</div>
        </Col> */}
    </Row>

    <FormGroup>
        <Label>{t("Description")}</Label>
        <Input
            type="textarea"
            value={description}
            disabled={state === "view"}
            onChange={this.onDescriptionChange}
        />
        {state !== "view" && (
            <FormFeedback
                invalid={
                    errors.description
                        ? "true"
                        : "false"
                }
            >
                {errors.description}
            </FormFeedback>
        )}
    </FormGroup>
</Form>
Mohamed Allal
  • 17,920
  • 5
  • 94
  • 97