"Semi-controlled" react 中的组件



我有通过 props 获取起始值的子组件。在组件中,此起始值被复制到本地状态,并且对其的所有修改都在组件本身中处理。

父组件通过事件接收修改后值的结果。

现在可以在父组件中修改值,并且此更改应反映在子组件中。

因此,子组件不是不受控制的,但也不是完全受控的。你可以说半控制。

下面是一个基本示例:

class ParentComponent extends React.Component {
state = {
value: 5
};
increment = () => {
this.setState({ value: this.state.value + 1 });
};
someOtherAction = () => {
this.forceUpdate();
};
render() {
return (
<div>
<button onClick={this.increment}>Parent modify</button>
<button onClick={this.someOtherAction}>Force rerender</button>
<br />
<br />
<ChildComponent value={this.state.value} onDone={val => alert(val)} />
</div>
);
}
}
class ChildComponent extends React.Component {
state = {
value: this.props.value
};
static getDerivedStateFromProps(props, state) {
return { value: props.value };
}
increment = () => {
this.setState({ value: this.state.value + 1 });
};
render() {
return (
<div>
<b>Child component</b> <br />
value: {this.state.value} <br />
<button onClick={this.increment}>Child modify</button>
<button onClick={() => this.props.onDone(this.state.value)}>
End modification
</button>
</div>
);
}
}

链接到工作示例

在示例中,一切正常,直到父组件由于与state.value字段无关的操作(由强制更新模拟(而更新,因为子值与父值不同步,因此更新后子值被父值覆盖。

可能的解决方案:

  1. 将"parentValue"状态添加到子组件,该状态始终设置为父组件的值。然后我可以getDerivedStateFromProps检查新的 props 值是否与当前父值不同,并且仅在这种情况时才更新值:

    static getDerivedStateFromProps(props, state) {
    return (props.value !== state.parentValue) ? { value: props.value, parentValue: props.value } : null;
    }
    

    问题:如果在子组件中修改了该值,并且我想在父组件中将其重置为旧的初始值 props.value 将等于 state.parentValue,因此不会发生更新。

  2. 添加一个 initialValue prop,用于初始化子组件上的值,然后仅在 props 包含值字段时才更新子组件的值。

你还有其他想法,如何处理?感谢您的帮助!

我认为您的问题在这篇官方反应博客文章中得到了很好的描述。它通常发生在父级通过传递 props "重置"子状态时。

确保在使用getDerivedStateFromProps()时永远不要用 props 覆盖状态,而无需任何进一步的检查。无条件地将 props 复制到状态被认为是一种反模式:

class EmailInput extends Component {
state = { email: this.props.email };
render() {
return <input onChange={this.handleChange} value={this.state.email} />;
}
handleChange = event => {
this.setState({ email: event.target.value });
};
componentWillReceiveProps(nextProps) {
// This will erase any local state updates!
// Do not do this.
this.setState({ email: nextProps.email });
}
}

有关避免这种情况的方法,请参阅博客文章中的首选解决方案。

解决方案

  • 最简单的解决方案是通过完全删除组件的状态或将其提升到父组件来使组件完全受控。

  • 另一种解决方案是使其完全不受控制,并为其提供唯一的key,以便在初始值更改时完全重新渲染:

    class EmailInput extends Component {
    state = { email: this.props.defaultEmail };
    handleChange = event => {
    this.setState({ email: event.target.value });
    };
    render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
    }
    }
    

    并用键渲染它:

    <EmailInput
    defaultEmail={this.props.user.email}
    key={this.props.user.id}
    />
    

    如果您现在希望组件"重置",只需将新的初始值与不同的键一起传递即可。React 将卸载旧组件并用新组件替换它。

当您要从父级更新状态时, 添加timestamp状态,并在每次状态更新时对其进行更新。

increment = () => {
this.setState({ value: this.state.value + 1,timestamp : (new Date()).getTime() });
};

内部渲染 :

<ChildComponent value={this.state.value} timestamp = {this.state.timestamp} onDone={val => alert(val)} />

现在,在子组件中,

state = {
value: this.props.value,
timestamp : this.prop.timestamp // store the passd value from prop
};

每次从父级重新渲染时,肯定更新timestamp仅可用 当它从increment方法而不是其他地方更新时。因此,您有办法检查父级通过正确的操作而不是强制更新触发的更新。

static getDerivedStateFromProps(props, state) {
return (props.timestamp > state.timestamp) ? { value: props.value, parentValue: props.value,timestamp : props.timestamp } : null;
}

我想如果不实现一些反模式,就没有一个非常好的解决方案。我要做的是使ChildComponent不受控制,并实现一个使ChildComponent完全受控的包装器。因此,根据我需要的任何东西,我要么直接使用 ChildComponent,要么使用包装器。

感谢您的帮助!

最新更新