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?
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With