【问题标题】:React Js component rendering only once after state changeReact Js 组件在状态更改后仅渲染一次
【发布时间】:2016-04-20 18:26:25
【问题描述】:

我对 ReactJS 很陌生,我的问题很不寻常,可能只是因为我没有按照他们的预期实现事情。

所以基本上来说,这之前工作得很好,但我需要添加一些新功能,而且......嗯,有些东西不对劲。

首先 - ConvFrame 是顶部组件,出现在页面顶部,它由 ConvForm 组件(用于添加新查询)和 ConvList 组成,其中未分配和新调用。 ConvList 这里的 ID 和 Key 为 1。

下面还有工作人员列表,他们只使用ConvForm,字段本身是dropzones,可以快速分配新的呼叫任务。 ConvList 这里的 Id 和 Key 等于 worker 的 id。

ConvList 被渲染时,它会向服务器查询列表中的作业。这对他们所有人都很好。但是,当通过ConvForm 添加新项目时,似乎出现了一些奇怪的问题。它调用handleCommentSubmit() 函数,调用this.loadCommentsFromServer();(愚蠢,我知道!)然后为记录设置新状态this.setState({records: data});

当添加第一条记录时,/api/zlecenia 会被调用两次。一次来自loadCommentsFromServer() 内部ConvFrame,第二次来自ConvList 内部。通过表单添加第二条记录调用一次,组件似乎对状态变化没有反应。我猜有些东西执行得很糟糕。

源代码如下: 对话.js

//For dragging
var placeholder = document.createElement("div");
placeholder.className = "placeholder";
var dragged;
var over;

/**
 * Conversation
 * Should be used for listing conversation blocks, adds class based on age of task.
 * Detects drag events, renders block, calls dragEnd function to append block to new
 * location and uses props.OnDrop function to pass id of element and target id of worker
 */
window.Conversation = React.createClass({
    dynamicClass: function () {
        return "convo " + this.props.delay;
    },
    dragStart: function (e) {
        dragged = e.currentTarget;
        over = null;
        e.dataTransfer.effectAllowed = 'move';
        e.dataTransfer.setData("text/html", e.currentTarget);
    },
    dragEnd: function (e) {
        $(dragged).show();
        $(placeholder).remove();
            console.log(over, over.className);
        if (over && over.className === "candrop") {
            var id = Number(dragged.dataset.id);

            this.props.onDrop({id: id, target: over.id});
            over.appendChild(dragged);
        }else{
            console.log('returning base:' + over);
           $(dragged).parent().append(dragged);
        }
    },
  render: function() {
    return (
      <div draggable="true" data-id={this.props.id} onDragEnd={this.dragEnd} onDragStart={this.dragStart} className={this.dynamicClass()}  >
        {this.props.children}
      </div>
    );
  }
});

/**
 * ConvList
 * Displays conversation dropdown list. I should aim to make it single component, do not repeat for workers.
 * Detects dragOver for .candrop and place some funny placeholder. Detect position change from Conversation view
 * call master class from parent component and pass data. Detect delete event.
 */
window.ConvList = React.createClass({
    getInitialState: function () {
        return {data: []};
    },
    loadConvsFromServer: function () {
        $.ajax({
            url: baseUrl + '/api/zlecenia',
            type: 'GET',
            data: {id: this.props.id},
            success: function (data) {
                this.setState({data: data});
            }.bind(this),
            error: function (xhr, status, err) {
                console.error(this.props.url, status, err.toString());
            }.bind(this)
        });
    },
    componentDidMount: function () {
        this.loadConvsFromServer();
    },
    dragOver: function (e) {
        e.preventDefault();
        $(dragged).fadeOut();
        if (e.target.className === "candrop") {
            if (e.target.className == "placeholder")
                return;
            over = e.target;
            e.target.appendChild(placeholder, e.target);
        }
    },
    updatePosition: function (data) {
        console.log('update convo %d for member %e', data.id, data.target);

        $.ajax({
            url: baseUrl + '/api/zlecenia',
            type: 'PUT',
            data: {id: data.id, assign: data.target},
            success: function (data) {
            }.bind(this),
            error: function (xhr, status, err) {
                console.error(this.props.url, status, err.toString());
            }.bind(this)
        });
    },
    deleteTask: function (e) {
        var taskIndex = parseInt(e.target.value, 10);
        console.log('remove task: %d', taskIndex);

        $.ajax({
            url: baseUrl + '/api/zlecenia/' + taskIndex,
            type: 'DELETE',
            success: function (data) {
                this.loadConvsFromServer();
            }.bind(this),
            error: function (xhr, status, err) {
                console.error(this.props.url, status, err.toString());
            }.bind(this)
        });

    },
    render: function () {
      return (
       <div className="convlist" onDragOver={this.dragOver}>
            <div className="candrop" id={this.props.id} >{this.props.id}
                {this.state.data.map((c) => 
                    <Conversation  onDrop={this.updatePosition} delay={c.delayTime} id={c.id} key={c.id}>
                        <p className="convTop">{c.formattedTime} | Tel. {c.phone} | {c.name} | Nr.Rej {c.number}</p>
                        <p>{c.text}</p>
                        <button className="deleteConv"  onClick={this.deleteTask} value={c.id}>x</button>
                     </Conversation>
                )}
            </div>
        </div>

      );
    }
});

/**
 * ConvForm
 * Displays conversation create form. Prepares fields, validates them.
 * Call master function to add new record on send.
 */
var ConvForm = React.createClass({
    getInitialState: function () {
        return {phone: '', name: '', number: '', text: ''};
    },
    handlePhoneChange: function (e) {
        this.setState({phone: e.target.value});
    },
    handleNameChange: function (e) {
        this.setState({name: e.target.value});
    },
    handleNumberChange: function (e) {
        this.setState({number: e.target.value});
    },
    handleTextChange: function (e) {
        this.setState({text: e.target.value});
    },
    submitForm: function (e) {
        e.preventDefault();
        var phone = this.state.phone.trim();
        var name = this.state.name.trim();
        var number = this.state.number.trim();
        var text = this.state.text.trim();

        if (!text || !phone || !name || !number) {
            return;
        }
        this.props.onConvSubmit({phone: phone, name: name, number: number, text: text});
        this.setState({phone: '', text: '', number: '', name: ''});
    },
    render: function () {
        return (
        <form className="convForm" onSubmit={this.submitForm}>
            <div className="row">
                <div className="col-xs-12 col-md-4">
                    <div className="form-group">
                        <input
                        className="form-control"
                        type="text"
                        placeholder="Telefon"
                        value={this.state.phone}
                        onChange={this.handlePhoneChange}
                        />
                    </div>
                </div>
                <div className="col-xs-12 col-md-4">
                    <div className="form-group">
                        <input
                        className="form-control"
                        type="text"
                        placeholder="Imię i nazwisko"
                        value={this.state.name}
                        onChange={this.handleNameChange}
                      />
                    </div>
                </div>
                <div className="col-xs-12 col-md-4">
                    <div className="form-group">
                        <input
                        className="form-control"
                        type="text"
                        placeholder="Nr. rejestracyjny"
                        value={this.state.number}
                        onChange={this.handleNumberChange}
                      />
                    </div>
                 </div>
            </div>
            <div className="form-group">
                <textarea
                className="form-control"
                type="text"
                placeholder="Treść"
                value={this.state.text}
                onChange={this.handleTextChange}
                />
            </div>
            <input className="btn btn-success" type="submit" value="Zapisz" />
        </form>
                );
    }
});

/**
 * ConvFrame
 * Conversation main frame and root functions for both form and conversations listing.
 */
window.ConvFrame = React.createClass({
    getInitialState: function () {
        return {records: []};
    },
    loadCommentsFromServer: function () {
        $.ajax({
            url: baseUrl + '/api/zlecenia',
            type: 'GET',
            data: {id : 1},
            success: function (data) {
                this.setState({records: data});
            }.bind(this),
            error: function (xhr, status, err) {
                console.error(this.props.url, status, err.toString());
            }.bind(this)
        });
    },
    handleCommentSubmit: function (convo) {
        $.ajax({
            url: baseUrl + '/api/zlecenia',
            type: 'POST',
            data: convo,
            success: function (data) {
                this.loadCommentsFromServer();
            }.bind(this),
            error: function (xhr, status, err) {
                console.error(this.props.url, status, err.toString());
            }.bind(this)
        });
    },
  render: function() {
        return (
                <div className="add-new">
                        <div className="row">
                            <div className="col-xs-12 col-md-12 frame">
                                <div className="col-xs-12 col-md-7">
                                    <h3>Dodaj nową rozmowę</h3>
                                    <ConvForm onConvSubmit={this.handleCommentSubmit} />
                                </div>
                                <div className="col-xs-12 col-md-5">
                                    <ConvList key='1' id='1'  data={this.state.records} />
                                </div>  
                            </div>
                        </div>
                </div>
                );
    }
});

workers.js

/**
 * WorkerList
 * 
 */
var Worker = React.createClass({
    render: function () {
      return (
         <div>
              {this.props.children}
         </div>
      );
    }
});


/**
 * WorkerList
 * 
 */
var WorkerList = React.createClass({
    render: function () {
      return (
       <div className="worker-list">
                {this.props.data.map((worker) => 
                    <Worker id={worker.id} key={worker.id}>
                        <div className="row">
                            <div className="col-xs-12 col-md-12 frame">
                                <div className="col-xs-12 col-md-5">
                                     <h4>{worker.username}</h4>
                                </div>
                                <div className="col-xs-12 col-md-7">
                                <ConvList key={worker.id} id={worker.id} />
                                </div>  
                           </div>
                        </div>
                    </Worker>
                )}

        </div>

      );
    }
});

/**
 * WorkerForm
 * 
 */
var WorkerForm = React.createClass({
  render: function() {
    return (
      <div>
      </div>
    );
  }
});


/**
 * WorkerFame
 * 
 */
window.WorkerFrame = React.createClass({
    getInitialState: function () {
        return {data: []};
    },
    loadWorkersFromServer: function () {
        $.ajax({
            url: baseUrl + '/api/pracownicy',
            type: 'GET',
            success: function (data) {
                this.setState({data: data});
            }.bind(this),
            error: function (xhr, status, err) {
                console.error(this.props.url, status, err.toString());
            }.bind(this)
        });
    },
    componentDidMount: function () {
        this.loadWorkersFromServer();
    },
    handleWorkerSubmit: function (worker) {
        $.ajax({
            url: baseUrl + '/api/pracownicy',
            type: 'POST',
            data: worker,
            success: function (data) {
                this.loadWorkersFromServer();
            }.bind(this),
            error: function (xhr, status, err) {
                console.error(this.props.url, status, err.toString());
            }.bind(this)
        });
    },
  render: function() {
        return (
                <div className="add-new">
                        <div className="row">
                            <div className="col-xs-12 col-md-12">
                                <WorkerList data={this.state.data} />
                            </div>  
                           <div className="col-xs-12 col-md-12">
                                <WorkerForm onWorkerSubmit={this.handleWorkerSubmit} />
                            </div>
                        </div>
                </div>
                );
    }
});

final.js

var DashFrame = React.createClass({
  render: function() {
    return (
      <div>
      <ConvFrame/>
      <WorkerFrame/>
      </div>
    );
  }
});


ReactDOM.render(
  <DashFrame/>,
  document.getElementById('dashboard')
);

任何提示将不胜感激。我的大脑在沸腾。

【问题讨论】:

  • 你有这个 plunker 页面吗?

标签: javascript reactjs


【解决方案1】:

从(长)代码中,我认为您的问题出现了,因为您在&lt;ConvList&gt; 中对服务器的调用位于componentDidMount() 内部:

componentDidMount: function () {
    this.loadConvsFromServer();
},

componentDidMount() 在初始挂载时总是只调用一次。

所以第二次,react 没有调用componentDidMount(),没有调用服务器,你的状态没有改变,因此&lt;ConvList&gt; 没有更新。

要修复,请添加:

componentDidUpdate: function () {
    this.loadConvsFromServer();
},

组件更新时也会调用。

更新:您还需要在结果 setState() 中添加条件,否则您将获得无限循环(componentDidUpdate() -> setState() -> componentDidUpdate() -> 重复)。

最好的地方可能在loadConvsFromServer() 里面。比如:

...
success: function (data) {
           var dataChanged = ...
           // some smart comparison of data with this.state.data
           if (dataChanged) {
             this.setState({data: data});
           }
         }.bind(this),
...

另外,我注意到submitForm() 内部&lt;ConvForm&gt; 内部的这个sn-p:

 this.props.onConvSubmit({phone: phone, name: name, number: number, text: text});
 this.setState({phone: '', text: '', number: '', name: ''});

这是一个危险的组合:第一次调用实质上将控制权交还给父级,父级可能(并且可能会)重新渲染整个树。紧接着,您使用setState() 触发第二次渲染。但无法确定他们将被解雇的顺序。
更清洁(并且更易于调试和维护)是将清除所有输入的setState() 移动到componentWillReceiveProps() 生命周期方法。这样,每次您的组件由其父组件重新渲染时,所有输入都将清除。奖励:您的组件只会重新渲染一次。

【讨论】:

  • 谢谢,但由于某种原因,它似乎不起作用。实际上,添加 componentDidUpdate 会导致服务器查询无休止的循环,一遍又一遍......如果你愿意,我可以压缩项目并将其邮寄给你或上传到某个地方。
  • 是的,它会导致死循环。您需要在 loadConvsFromServer() 中添加一些条件,以便仅在内容发生变化时才执行 setState()。将编辑。
  • 谢谢。你救了我一个不眠之夜。
猜你喜欢
  • 2020-04-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-07-28
相关资源
最近更新 更多