我有一个非常简单的组件,带有文本字段和按钮:
它接受一个列表作为输入,并允许用户循环浏览该列表。
该组件有以下代码:
import * as React from "react";
import {Button} from "@material-ui/core";
interface Props {
names: string[]
}
interface State {
currentNameIndex: number
}
export class NameCarousel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentNameIndex: 0}
}
render() {
const name = this.props.names[this.state.currentNameIndex].toUpperCase()
return (
<div>
{name}
<Button onClick={this.nextName.bind(this)}>Next</Button>
</div>
)
}
private nextName(): void {
this.setState( (state, props) => {
return {
currentNameIndex: (state.currentNameIndex + 1) % props.names.length
}
})
}
}
这个组件工作得很好,除了我没有处理过状态改变时的情况。当。。。的时候
状态发生变化,我想重置currentNameIndex
为零。
做这个的最好方式是什么?
我考虑过的选项:
Using componentDidUpdate
这个解决方案很笨拙,因为componentDidUpdate
渲染后运行,所以我需要添加一个子句
在组件处于无效状态时,在渲染方法中“不执行任何操作”,如果我不小心,
我可以导致空指针异常。
我在下面包含了这个的实现。
Using getDerivedStateFromProps
The getDerivedStateFromProps
方法是static
并且签名仅允许您访问
当前状态和下一个道具。这是一个问题,因为你无法判断 props 是否已更改。作为
结果,这迫使您将道具复制到状态中,以便您可以检查它们是否相同。
使组件“完全受控”
我不想这样做。该组件应该私有地拥有当前选择的索引。
使组件“完全不受密钥控制”
我正在考虑这种方法,但不喜欢它如何导致父母需要了解
孩子的实施细节。
Link https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key
Misc
我花了很多时间阅读您可能不需要派生状态 https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key但我对那里提出的解决方案非常不满意。
我知道这个问题的变体已经被问过多次,但我不认为任何答案都会权衡可能的解决方案。一些重复的例子:
- 如何在道具更改时重置组件中的状态 https://stackoverflow.com/questions/59343481/how-to-reset-state-in-a-component-on-prop-change
-
Update component state when props change https://stackoverflow.com/questions/54441229/update-component-state-when-props-change
- 在 React Form 中更新 props 变化的状态 https://stackoverflow.com/questions/32414308/updating-state-on-props-change-in-react-form
Appendix
解决方案使用componetDidUpdate
(见上面的描述)
import * as React from "react";
import {Button} from "@material-ui/core";
interface Props {
names: string[]
}
interface State {
currentNameIndex: number
}
export class NameCarousel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentNameIndex: 0}
}
render() {
if(this.state.currentNameIndex >= this.props.names.length){
return "Cannot render the component - after compoonentDidUpdate runs, everything will be fixed"
}
const name = this.props.names[this.state.currentNameIndex].toUpperCase()
return (
<div>
{name}
<Button onClick={this.nextName.bind(this)}>Next</Button>
</div>
)
}
private nextName(): void {
this.setState( (state, props) => {
return {
currentNameIndex: (state.currentNameIndex + 1) % props.names.length
}
})
}
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>): void {
if(prevProps.names !== this.props.names){
this.setState({
currentNameIndex: 0
})
}
}
}
解决方案使用getDerivedStateFromProps
:
import * as React from "react";
import {Button} from "@material-ui/core";
interface Props {
names: string[]
}
interface State {
currentNameIndex: number
copyOfProps?: Props
}
export class NameCarousel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentNameIndex: 0}
}
render() {
const name = this.props.names[this.state.currentNameIndex].toUpperCase()
return (
<div>
{name}
<Button onClick={this.nextName.bind(this)}>Next</Button>
</div>
)
}
static getDerivedStateFromProps(props: Props, state: State): Partial<State> {
if( state.copyOfProps && props.names !== state.copyOfProps.names){
return {
currentNameIndex: 0,
copyOfProps: props
}
}
return {
copyOfProps: props
}
}
private nextName(): void {
this.setState( (state, props) => {
return {
currentNameIndex: (state.currentNameIndex + 1) % props.names.length
}
})
}
}