在React中处理受控表单组件更改

时间:2016-08-26 11:49:05

标签: javascript forms reactjs

TLDR:在不违反React原则或忽略标准封装机会的情况下,React似乎无法达到通用设计要求。

我有一个表单,显示来自服务器API的现有数据,并允许用户编辑字段并更新服务器上的数据。这是在用户更改输入值时动态发生的,而不是要求他们在每次编辑后“提交”表单。

对于某些表单输入,会立即更改值(例如复选框,单选按钮,选择)。对于其他人来说,价值变化是逐步增加的 - 最明显的是文本输入。我不希望每次击键都有服务器请求,因为这会导致为不完整的值生成服务器端验证错误。相反,一旦用户离开文本输入字段,服务器就会更新。如果值不变,发送服务器请求也是一种浪费。

在React中,关键设计原则似乎是你保持一个真实/状态的单一来源,并通过使用道具的组件渗透。组件不应该保持自己的状态作为道具的副本,而是应该直接渲染道具[1]。 “单一来源”将是从服务器拉出数据的父组件。

对于呈现的<input>元素,要使其值与服务器更改保持同步,它应使用value属性,因为defaultValue仅在第一次渲染时评估[2]。 React设计原则意味着我应该设置<input value={this.props.value} />。为了响应用户输入,还必须提供onChange处理程序,将更改冒泡到父组件,这将更新状态并导致<input>重新呈现更新后的props

但是,我不想在onChange处理程序中触发服务器请求,因为这会在每次击键时触发。我需要在onBlur事件上触发服务器请求,假设自onFocus以来该值已更改。对于某些元素而不是其他元素需要这个元素意味着父组件需要两个处理程序:onChange处理程序来更新所有子组件的状态并触发服务器对某些字段的请求,并onBlur为触发其他字段的服务器请求。要求父组件知道哪些子表单组件应该表现出哪种行为似乎无法正确封装。子组件应该能够监视自己的值并决定何时发出“做某事”事件。

我无法在不违反React原则的情况下找到实现这一目标的方法,最有可能的方法是在每个表单组件中维护一个状态。像这样:

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} />);
    }
}

有没有更好的方法来实现这一点,而不会违反React的渲染道具的原则来呈现?

进一步阅读[3-4]中解决此问题的各种尝试。

[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?

3 个答案:

答案 0 :(得分:3)

你已经花了很多时间思考这个问题并查找相关的讨论。对于它的价值,我认为你实际上是 over - 思考这个问题。 React的核心是非常实用工具。它会向您推动某些方向,并且肯定会被认为是惯用的方法,但它提供了许多逃生舱,使您可以打破基本方法以适应您的特定用例。因此,如果您需要以一种特定的方式构建事物以使您的应用程序按照您希望的方式运行,请不要花时间担心它是否“绝对完全符合React的原则”。偏离某些神话般的理想,没有人会对你大喊:)

是的,您提供的示例代码乍一看似乎非常合理。

答案 1 :(得分:2)

所以,你正走在正确的轨道上。我知道这种事情起初有多混乱。

我要做的第一件事是稍微重构你的TextInput是一个完全愚蠢的组件,基本上是一个没有自己状态的组件。

我们将通过道具明确处理此组件的所有内容。与该组件的状态相关的所有内容(值,处理程序)将进入您的FormHandler。我之所以采用这种方式,是因为它可以更轻松地处理来自API的数据,同时保持您的TextInput组件足够灵活,几乎可以在您的网络应用中的任何位置重复使用。

所以,我们要去掉很多东西!我们走了:

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}
            />
        );
    }
}

那是什么?!是啊。因此,正如我之前所说,我们会将大量内容移到您的FormHandler组件中。它看起来会像这样。

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}
            />);
    }
}

因此,从这里开始,您可以将FormHandler中的state.value设置为您想要的默认值。或者只是等待来自API的响应,一旦事情更新就会传递回来(不是最理想的用户体验 - 你可以开始添加加载微调器和所有这些东西,如果你想要的话)。

编辑:根据Reddit的讨论,修复我们如何处理FormHandler内的功能绑定。

答案 2 :(得分:0)

我也喜欢其他答案:)

  

在React中,关键的设计原则似乎是你保持一个真实/状态的单一来源,并使用道具渗透到组件中。

我认为如果你忽略了React的setState功能,你只能说这个,我不认为你应该忽略它,因为它在教程中。

但是,由于我知道我们正在使用MobX,您还应该看到:https://medium.com/@mweststrate/3-reasons-why-i-stopped-using-react-setstate-ab73fc67a42e#.uak5sug89

  

将更改冒泡到父组件...要求父组件知道哪些子表单组件应该表现出哪种行为似乎无法正确封装。

我不相信这个逻辑必须在父母中完成。我可以很容易地想象一个可重用的组件,它本身就可以完成一些逻辑。

  

子组件应该能够监视自己的值并决定何时发出“做某事”事件。

同意。

来自@markerikson:

  

你已经花了很多时间思考这个问题并查找相关的讨论。对于它的价值,我认为你实际上是在过度思考这个问题。

我怀疑他是对的。