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?