Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling controlled form component changes in React

TLDR: Common design requirement seems impossible in React without violating React principles or ignoring standard encapsulation opportunities.

I have a form that displays existing data from a server API, and allows the user to edit the fields and update the data on the server. This happens dynamically as the user changes form input values, rather than requiring them to 'submit' the form after each edit.

For some form inputs, the value change is made immediately (e.g. checkbox, radio button, select). For others, the value change is made incrementally - most obviously for the text input. I would not want a server request on every keystroke, as this will lead to server-side validation errors being generated for incomplete values. Instead, the server would be updated once the user leaves the text input field. It would also be a waste to send a server request if the values are unchanged.

In React, the key design principle seems to be that you maintain a single source of truth/state and have this permeate down through components using props. Components should not maintain their own state as a copy of the props, but instead should render the props directly [1]. The 'single source' would be the parent component that pulls and pushes data from and to the server.

For a rendered <input> element to keep its value up to date with server changes, it should use a value attribute, since defaultValue is only evaluated on first render [2]. The React design principles imply I should set <input value={this.props.value} />. In order for this to respond to user input, an onChange handler must also be provided, bubbling the change up to the parent component, which will update the state and cause the <input> to be re-rendered with an updated props.

However, I would not want to trigger a server request in the onChange handler, since this will trigger on every keystroke. I would need to trigger the server request instead on an onBlur event, supposing that the value has changed since the onFocus. Requiring this for some elements and not others means that the parent component would need two handlers: an onChange handler to update the state for all child components and fire a server request for certain fields, and an onBlur to fire a server request for the other fields. Requiring the parent component to know which of the child form components should exhibit which behaviour seems like a failure to encapsulate properly. The child components should be able to monitor their own values and decide when to emit a 'do something' event.

I cannot see a way to achieve this without violating one of the React principles, most likely by maintaining a state within each form component. Something like this:

class TextInput extends React.Component {
    constructor(props) {
        super(props);
        this.initialValue = this.props.value;
        this.setState({value: this.props.value});
    }
    componentWillReceiveProps = (nextProps) => {
        this.initialValue = nextProps.value;
        this.setState({value: nextProps.value});
    };
    handleFocus = (e) => {
        this.initialValue = e.target.value;
    };
    handleChange = (e) => {
        this.setState({value: e.target.value});
    };
    handleBlur = (e) => {
        if (e.target.value !== this.initialValue &&
            this.props.handleChange) {
                this.props.handleChange(e);
        }
    };
    render() {
        return (
            <input type="text"
                   value={this.state.value}
                   onFocus={this.handleFocus}
                   onChange={this.handleChange}
                   onBlur={this.handleBlur} />
        );
    }
}

class FormHandler extends React.Component {
    componentDidMount() {
        // fetch from API...
        this.setState(apiResponse);
    }
    handleChange = (e) => {
        // update API with e.target.value....
    };
    render() {
        return (<TextInput value={this.state.value}
                           handleChange={this.handleChange} />);
    }
}

Is there a better way of achieving this, without breaking React's principles of trickling props down to be rendered?

Further reading of various attempts to resolve this in [3-4].

Other SO questions from people struggling with a similar problem in [5-6].

[1] https://facebook.github.io/react/tips/props-in-getInitialState-as-anti-pattern.html

[2] https://facebook.github.io/react/docs/forms.html#default-value

[3] https://discuss.reactjs.org/t/how-to-pass-in-initial-value-to-form-fields/869

[4] https://blog.iansinnott.com/managing-state-and-controlled-form-fields-with-react/

[5] How do I reset the defaultValue for a React input

[6] Using an input field with onBlur and a value from state blocks input in Reactjs JSX?

like image 904
practual Avatar asked Aug 26 '16 11:08

practual


Video Answer


2 Answers

You've clearly spent a lot of time thinking about this and looking up related discussions. For what it's worth, I think you're actually over-thinking the issue. React, at its core, is a very pragmatic tool. It pushes you in certain directions, and there's definitely approaches that are considered to be idiomatic, but it provides a number of escape hatches that allow you to break out of the basic approaches to fit your specific use case. So, if you need to architect things a specific way to make your app behave the way you want, don't spend time worrying about whether it's "absolutely completely in conformance with React's principles". No one is going to yell at you for deviating from some mythical ideal :)

And yes, the sample code you provided seems perfectly reasonable at first glance.

like image 69
markerikson Avatar answered Sep 22 '22 04:09

markerikson


So, you're on the right track. I know how confusing this sort of thing is at first.

The first thing I would do is slightly refactor your TextInput to be a completely dumb component, basically a component that doesn't have its own state.

We're going to explicitly handle everything for this component through props. All the stuff related to the state of this component (value, handlers) are going to move into your FormHandler. The reason I go this route is that it makes it much easier to work with data from an API while keeping your TextInput component flexible enough to reuse nearly anywhere in your web app.

So, we're going to get rid of a lot of stuff! Here we go:

class TextInput extends React.Component {
    render() {
        return (
            <input
                type="text"
                onBlur={() => this.props.handleBlur()}
                onChange={(e) => this.props.handleChange(e.target.value)}
                value={this.props.value}
            />
        );
    }
}

That's it?! Yeah. So, like I said earlier, we're going to move a lot of that stuff into your FormHandler component. It's going to look like this.

class FormHandler extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            value: this.props.value
        });

        this.handleBlur = this.handleBlur.bind(this)
        this.handleChange = this.handleChange.bind(this)
    }
    componentDidMount() {
        // fetch from API...
        this.setState(apiResponse);
    }
    handleBlur() {
        // Send a request to API using value four inside this.state.value.
        // This provides the user with instant feedback on the form, 
        // but still lets you do an async call to API to make sure things actually get updated.
    }
    handleChange = (input) => {
        this.setState({value: input});
    };
    render() {
        return (
            <TextInput
                handleBlur={this.handleBlur}
                handleChange={this.handleChange}
                value={this.state.value}
            />);
    }
}

So, from here you could set state.value in your FormHandler to be whatever default value you want. Or just wait for a response from the API, which will get passed down once things update (not the most ideal user experience -- you can start adding loading spinners and all that stuff if you want).

EDIT: Fix how we handled function binding inside FormHandler based on a discussion at Reddit.

like image 24
Dave Avatar answered Sep 23 '22 04:09

Dave