在另一个元素上反应setState

时间:2018-11-02 19:29:38

标签: javascript reactjs

我是新来的反应者,我正在将现有项目从jQuery转换为React。

我有六个选择框,它们会根据前一个选择框的选择进行顺序更新,例如FOOselect box A中的选择选项select box B必须更新为与FOO相对应的项目。

我将在底部列出一些参考

我到目前为止所拥有的: 我有onchange个事件,它们使用fetch调用了我的api,并获取了我想用来填充下一个选择框的数据,这就是我碰壁的地方。

我已经写了两个组件MapControlsSelectBoxMapControls处于其状态的一组对象,这些对象用于生成SelectBox个实例的集合

这是MapControls组件:

class MapControls extends React.Component {

state = {
    selectBoxes: [
        {
            id: 'WorkSource',
            name: 'WorkSource',
            title:'Work Source',
            containerId: 'WorkSourceContainer',
            className: 'WorkSource',
            childControllerMethod: 'GetBusinessTypeDropdown',
            items: [{value:0, text:'Select'}, { value: '1', text: 'Routed' }],
            child: 'BusinessType'
        },
        {
            id: 'BusinessType',
            name: 'BusinessType',
            title: 'Business Type',
            containerId: 'BusinessTypeContainer',
            className: 'BusinessType',
            childControllerMethod: 'GetWorkTypeDropdown',
            items: [{ value: 0, text: 'Select' }],
            child: 'WorkType'
        },
        //... more items ...
    ]
}

render() {
    return this.state.selectBoxes.map(selectBox => 
        <div key={selectBox.id} className='col-xs-2'>
            <div id={selectBox.containerId}>
                <SelectBox id={selectBox.id} name={selectBox.name} selectBox={selectBox} onChange={this.handleChange} />
            </div>
        </div>
    );

}
};

,这里是SelectBox组件。我想在handleChange事件中基于SelectBox更新另一个ref实例中的项目。查看描述我绊脚石的内联注释

class SelectBox extends React.Component{

constructor(props) {
    super(props);
    this.state = { items: this.props.selectBox.items };
    this.handleChange = this.handleChange.bind(this);
}

handleChange(event) {
    const selectedValue = event.target.value;
    const url = "/Home/" + event.target.dataset.childControllerMethod;
    const data = JSON.stringify({ selectedValue: selectedValue });

    fetch(url, {
        method: 'post',
        headers: {
            'Accept': 'application/json, text/plain, */*',
            'Content-Type': 'application/json'
        },
        body: data
    }).then(response => {
        if (response.status >= 400) {
            console.log("Bad response from server");
        }
        return response.json();
    }).then(data => {


        // This updates the selectbox that was changed, which is not what I want
        // this.setState({ items: data})  

        // This is what I was hoping would work, but I've discovered that BusinessType is a DOM element here, so setState is not valid
        // this.refs.BusinessType.setState({ items: data });

        // I hardcorded the 'BusinessType' as a ref just for testing because I know it's valid, but I want this to be dynamic
        // findDOMNode seems to be somewhat of an anti-pattern, so I'd rather not do this. Not that the below code works because sibling is not a React object
        // let sibling = ReactDOM.findDOMNode(this.refs.BusinessType);
        // sibling.setState({ items: data });   
    });
}


render() 
{

    const optionItems = this.state.items.map((item, index) =>
        <option key={index} value={item.value} >{item.text}</option>
    );



    return <div>
        <label htmlFor={this.props.selectBox.id} >{this.props.selectBox.title}</label>
        <select onChange={this.handleChange} id={this.props.selectBox.id} ref={this.props.selectBox.child} /*data-child={this.props.selectBox.child}*/ data-child-controller-method={this.props.selectBox.childControllerMethod}>
                {optionItems}
            </select>

        </div>
}
};

ReactDOM.render(<MapControls />,
document.getElementById('mapControls')
);

我看过的地方:

2 个答案:

答案 0 :(得分:1)

您似乎想要的类似于使用@input @output的Angular的双向绑定。

您可以执行以下操作:

class MapControls extends React.Component{
    constructor(props){
        super(props); // needed
        this.state = {...} // Your declared state above
        this.handleChange = this.handleChange.bind(this);
    }      

    handleChange(data){
       // Here you should receive data change emitted from child components

    }

    render(){
        ...
        <SelectBox id={selectBox.id} name={selectBox.name} selectBox={selectBox} onChange={this.handleChange}

    }
}

句柄更改侦听器应在父组件上发生,请考虑将fetch命令移至父组件。 您需要向父母发送的是孩子的event.target

class SelectBox extends React.Component{

constructor(props) {
    super(props);
    this.state = { items: this.props.selectBox.items };
    this.emitChange = this.emitChange.bind(this);
    // Changed funciton's name to emitChange to avoid confusion
}

emitChange(event) {
    const selectedValue = event.target.value;
    const url = "/Home/" + event.target.dataset.childControllerMethod;
    const data = JSON.stringify({ selectedValue: selectedValue });

    fetch(url, {
        method: 'post',
        headers: {
            'Accept': 'application/json, text/plain, */*',
            'Content-Type': 'application/json'
        },
        body: data
    }).then(response => {
        if (response.status >= 400) {
            console.log("Bad response from server");
        }
        return response.json();
    }).then(data => {
        // While you could keep this here, it can be sent to parent, it's your decision
        if(!!this.props.onChange){
            // Here you'll emit data to parent via a props function
            this.props.onChange(data);
        }
    });
}


render() {

    const optionItems = this.state.items.map((item, index) =>
        <option key={index} value={item.value} >{item.text}</option>
    );



    return <div>
        <label htmlFor={this.props.selectBox.id} >{this.props.selectBox.title}</label>
        <select onChange={this.emitChange} id={this.props.selectBox.id} ref={this.props.selectBox.child} /*data-child={this.props.selectBox.child}*/ data-child-controller-method={this.props.selectBox.childControllerMethod}>
                {optionItems}
            </select>

        </div>
}
};

ReactDOM.render(<MapControls />,
document.getElementById('mapControls')
);

所以,这是一个一般性的想法,您从父级传递了一个绑定有功能的道具(父级),子级将有一个执行道具的方法(如果存在)。

我在此示例中遗漏的内容:

您需要考虑在哪里处理fetch命令(父项或子项),请记住,如果props更改,则不会更新构造函数中定义的状态。

如果要状态更新组件的属性更改,则必须使用“ componentWillReceiveProps”(在最近版本中已弃用)等事件周期。

我的一般建议是,子组件应该放在道具上,而父组件应该处理要作为道具传递给孩子的状态

将函数句柄作为道具传递是相互交流组件的好方法,您还可以使用RXJS并将 Subscription 类型作为道具传递。

答案 1 :(得分:0)

所以我找到的解决方案如下。感谢Gabriel为我指出正确的方向。最终解决方案可用于需要对用户选择做出反应的任何过滤器组件

我遵循加百列的建议,调用了父对象的onChange事件并在那里处理状态设置。

我创建了triggerSibling方法,这样我就可以加入componentDidUpdate()事件并在选择框的层次结构中级联更改。因此onChangecomponentDidMount事件触发了相同的逻辑。

然后在MapControls onChange中,我按照Gabriel的建议处理那里的数据。

在对父对象的onChange事件的调用中,我将api调用中的数据以及要定位的孩子的名字一起传递给了

可以通过this.refs访问父组件的子组件,我发现可以通过使用其名称作为子组件数组中的键来访问特定的子组件,如下所示 this.refs[data.child].setState({ items: data.items })

我使用componentDidMount()事件设置了第一个selectBox的初始值,并在初始加载时触发了级联更新

MapControls组件:

class MapControls extends React.Component {
constructor(props) {
    super(props); // needed
    this.state = {
        selectBoxes: [
            {
                id: 'WorkSource',
                name: 'WorkSource',
                title: 'Work Source',
                containerId: 'WorkSourceContainer',
                className: 'WorkSource',
                childControllerMethod: 'GetBusinessTypeDropdown',
                items: [{ value: 0, text: 'Select' }, { value: 'ROUTED', text: 'Routed' }],
                child: 'BusinessType'
            },
            {
                id: 'BusinessType',
                name: 'BusinessType',
                title: 'Business Type',
                containerId: 'BusinessTypeContainer',
                className: 'BusinessType',
                childControllerMethod: 'GetWorkTypeDropdown',
                items: [{ value: 0, text: 'Select' }],
                child: 'WorkType'
            },
            ... more ...
        ]
    }
    this.handleChange = this.handleChange.bind(this);
}      


handleChange(data) {
    this.refs[data.child].setState({ items: data.items });

}


render() {
    return this.state.selectBoxes.map(selectBox => 
        <div key={selectBox.id} className='col-xs-2'>
            <div id={selectBox.containerId}>
                <SelectBox id={selectBox.id} name={selectBox.name} ref={selectBox.name} selectBox={selectBox} onChange={this.handleChange} />
            </div>
        </div>
    );

}
};

SelectBox组件:

class SelectBox extends React.Component{

constructor(props) {
    super(props);
    this.state = { items: this.props.selectBox.items };
    this.emitChange = this.emitChange.bind(this);
}


triggerSibling (idOfDropdownToUpdate, selectedValue, url) {

    const data = JSON.stringify({ selectedValue: selectedValue });

    fetch(url, {
        method: 'post',
        headers: {
            'Accept': 'application/json, text/plain, */*',
            'Content-Type': 'application/json'
        },
        body: data,
    }).then(response => {
        if (response.status >= 400) {
            console.log("Bad response from server");
        }
        return response.json();
    }).then(data => {

        if (!!this.props.onChange) {
            // add the target to be updated as `child` property in the data passed to the parent
            this.props.onChange({ child: this.props.selectBox.child, items: data });
        }
    });
}

componentDidMount() {
    // Set the value of the first selectBox AFTER it has mounted, so that its `onChange` event is triggered and the `onChange` events cascade through the rest of the selectBoxes
    if (this.props.name == "WorkSource") {
        this.setState({ items: [{ value: 'ROUTED', text: 'Routed' }] });
    }
}

// triggered when the component has finished rendering
componentDidUpdate(prevProps, prevState) {
    const url = "/Home/" + this.props.selectBox.childControllerMethod;
    if (this.props.selectBox.child != "")
        this.triggerSibling(this.props.selectBox.child, this.state.items[0].value, url)
}

emitChange(event) {
    const idOfDropdownToUpdate = event.target.dataset.child;
    const selectedValue = event.target.value;
    const url = "/Home/" + event.target.dataset.childControllerMethod;
    this.triggerSibling(idOfDropdownToUpdate, selectedValue, url)
}


render() 
{

    const optionItems = this.state.items.map((item, index) =>
        <option key={index} value={item.value} >{item.text}</option>
    );



    return <div>
        <label htmlFor={this.props.selectBox.id} >{this.props.selectBox.title}</label>
        <select onChange={this.emitChange} id={this.props.selectBox.id} data-child={this.props.selectBox.child} data-child-controller-method={this.props.selectBox.childControllerMethod}>
                {optionItems}
            </select>

        </div>
}
};