ReactJS表单组件的最佳实践

时间:2014-10-29 09:15:05

标签: javascript forms reactjs

我正在寻找最佳做法,让ReactJS组件负责用户编辑给定实体的表单。这里非常简化的例子。在许多情况下,实际表单会有更多字段和更多GUI功能。

React.createClass({
    getInitialState: function() {
        return {
            entity: {
                property1: null,
                property2: null
            }
        };
    },

    handleChange: function(e) {
        var entity = this.state.entity;

        switch(e.target.name) {
            case 'property1':
                entity.property1 = e.target.value;
                break;
            case 'property2':
                entity.property2 = e.target.value;
                break;
        }

        this.setState({
            entity: entity
        });
    },

    render: function() {
        return (
            <div className="entity-form">
                <form onChange={this.handleChange}>
                    <input type="text" name="property1" value={this.state.entity.property1} />
                    <br />

                    <textarea name="property2" value={this.state.entity.property2}></textarea>
                    <br />
                </form>
            </div>
        );
    }
});

表单的字段直接编辑实体对象,然后可以将其保存到RESTful api。我希望在用户更改字段时更新组件,因此GUI可以在键入期间根据输入做出反应(如验证,信息等)。

理论上,我可以让整个状态对象代表正在编辑的实体,因此实体的每个属性都是第一级状态变量。但是,我希望能够为GUI函数添加额外的状态变量以及与组件将要执行的操作相关的其他事情,因此我更希望实体对象是一个状态变量,如&#34; entity&#34;上面的状态变量。该对象当然可以是一些更复杂的对象,如Backbone模型或类似对象,但在这个简化的示例中,我只使用一个简单的对象,而不是所需的属性。

因此,为了寻找为此目的制作React组件的最佳实践方法,我有一些问题:

  1. 道具或州。
  2. 在这种情况下,我选择将带有表单内容的实体对象放在状态变量而不是prop中。这是为了能够在表单输入期间更新对象,而无需调用父级并更新道具。就我的React经验而言,这对于像这样的表单组件来说是最好的做法。

    1. 受控或不受控制的输入。
    2. 在上面的简化示例中,我使用受控输入。这导致更新状态并在每次更改时重新呈现组件(例如,输入文本字段的每个字符)。这是最好的做法吗?好处是组件可以完全控制发生的事情,而不是使用defaultValue参数,并且在某些事件上(如用户按下保存按钮),组件提取值,更新实体并将其保存到服务器。如果在这种情况下应该使用受控或不受控制的输入,是否有任何理由(或意见)?

      1. onChange表单或每个输入
      2. 该示例在表单标记上有一个onChange,每次更改表单中的任何字段时,都会调用handleChange方法。但是,由于输入是受控制的(具有值参数),React会抱怨输入字段没有onChange属性。这是否意味着在表单标记上有一个共同的onChange是不好的做法,我应该删除它并在每个字段上放置onChange?

        1. 更新个别属性
        2. 在上面的例子中,我使用一个基于正在更新的输入字段的开关(当调用handleChange时)。我想我可以确保所有字段名称与实体的属性名称同步,并且我可以根据事件(e.target.name)中字段的名称在handleChange中设置实体对象的属性。但是,这使得每个字段很难满足个别需求,即使大多数字段只是直接更新实体属性。我猜一个alternativ是一个基于输入名称的默认块设置的开关,以及需要其他更新方式的任何字段的案例块(比如在实体上设置它之前过滤该值)。如果您知道以这种方式更新现场更新的方法,请对此进行评论。

          1. 更新州实体
          2. 此示例的一个大问题是实体对象的更新方式。由于handleChange中的实体变量从当前状态设置为实体对象,因此这只是一个指针,更新实体变量将更改状态中的对象。 React页面说你不应该直接更新状态。其中一个原因是我在调用setState之前以这种方式更新状态时遇到的问题。如果具有shouldComponentUpdate方法,则prevState包含新状态,因为发送到shouldComponentUpdate的prevState参数的内容基于调用setState时的状态。据我所知,没有简单的方法来克隆javascript中的对象。所以问题是,当我需要更新整个对象(而不是触及对象中的其他值)而不是仅运行单个状态变量的setState时,最好的方法是在不引起类型的情况下执行此操作国家混淆?

1 个答案:

答案 0 :(得分:6)

  1. 任何会改变的事情都发生在州。
  2. 如果您正在查看加载现有实体并进行编辑,则需要受控输入,并且您希望相应地设置值。在大多数情况下(在下拉列表之外),我倾向于远离defaultValue
  3. 这与您之前的问题有关。如果指定一个值,则表示您正在使用受控输入,并且必须为任何受控输入提供onChange处理程序,否则它将一成不变。受控输入的好处是用户无法编辑它们,所以如果你有一些属性被锁定(可能是为了只读,出于安全原因),当用户试图保存时,即使他们直接编辑了HTML,React应该拉来自vDOM表示的属性值(这里可能是错误的,但我相信我之前已经测试过了)。无论如何,你必须直接在受控输入上设置onChange。至于使用事件delgation(在表单级别),对于很多事件来说这根本不是一个坏习惯,这只是一个特定的场景(受控输入),你需要为每个元素指定onChange事件。
  4. 不完全确定这个问题是什么,但是我使用了ref而不是target.name。
  5. 所以,你是正确的,你永远不应该直接改变状态,这是文档中的一个棘手的问题。 React将直接改变状态,它只是通过setState在实现中执行。如果在此方法调用之外更改状态,则会发生意外情况并引发错误。
  6. shouldComponentUpdate只进行浅层比较,但这里有一些解决方案。

    一个是对对象进行字符串化,这是一个快速而肮脏的对象比较,不是真的推荐它,但它有效。

    一个更好的解决方案,我和React + Flux一起使用的是实现propertyChanged bool,并在你的shouldComponentUpdate中检查它。

    现在,这将要求您在事情发生变化时意识到设置它,即您在对象图中更改了更深层次的内容。假设propertyOne是一个对象,其属性在handleChange方法中被更改。您可以根据需要验证输入,然后设置propertyChanged = true,,然后您需要实现componentDidUpdate 。我们在此做出假设,但如果组件已更新,则将propertyChanged设置为false,这样您就不会再触发不需要的更新。我希望这是有道理的。它有点像单向的notifyPropertyChanged。

    我提供了一个快速示例,说明我可能会为更加动态的实现做些什么,它允许您向对象添加更多属性(在此实现中只有浅层属性,显然您可以编写更强大的解决方案)。如果您有任何其他问题,或者我没有回答,请告诉我。

    http://jsfiddle.net/rpv9trhh/

    var e = {
        prop1: 'test',
        prop2: 'wee',
        prop3: 'another property',
        propFour: 'Oh yeah!'
    };
    
    var FormComp = React.createClass({
        getInitialState: function(){
            return {
                entity: this.props.entity
            }
        },
        render: function() {
    
            var ent = this.state.entity;
            var that = this;
            var inputs = [];
    
            for(var key in ent){
                inputs.push(<input 
                   key={key} 
                   style={{display:'block'}} 
                   type="text" 
                   ref={key} onChange={that._propertyChanged.bind(null, key)}
                   value={ent[key]} />)
            }
    
            return <form>
                {inputs}
                <input 
                    type="button" 
                    onClick={this._saveChanges} 
                    value="Save Changes" />
            </form>;
        },
        _propertyChanged: function(propName) {
            var nextProp = this.refs[propName].getDOMNode().value;
            var nextEntity = this.state.entity;
            nextEntity[propName] = nextProp;
    
            this.setState({
                entity: nextEntity
            });
        },
        _saveChanges: function() {
            var updatedEntity = this.state.entity;
    
            for(var key in updatedEntity){
                alert(updatedEntity[key]);
            }
    
            //TODO: Call to service to save the entity, i.e.    
            ActionCreators.saveEntity(updatedEntity);
        }
    });
    
    React.renderComponent(<FormComp entity={e} />, document.body);