繁体   English   中英

以下哪种策略是在 props 更改时重置组件状态的最佳方法

[英]Which of these strategies is the best way to reset a component's state when the props change

我有一个非常简单的组件,带有一个文本字段和一个按钮:

<图片>

它接受一个列表作为输入,并允许用户在列表中循环。

该组件具有以下代码:

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重置为零。

做这个的最好方式是什么?


我已经考虑的选项:

使用componentDidUpdate

这个解决方案是ackward,因为componentDidUpdate在render之后运行,所以我需要在render方法中添加一个子句,当组件处于无效状态时“什么都不做”,如果我不小心,我可能会导致空指针-例外。

我在下面包含了一个实现。

使用getDerivedStateFromProps

getDerivedStateFromProps方法是static ,签名只允许您访问当前状态和下一个道具。 这是一个问题,因为您无法判断道具是否已更改。 结果,这迫使您将道具复制到状态中,以便您可以检查它们是否相同。

使组件“完全受控”

我不想这样做。 这个组件应该私有拥有当前选择的索引是什么。

使组件“完全不受钥匙控制”

我正在考虑这种方法,但不喜欢它如何导致父级需要了解子级的实现细节。

关联


杂项

我花了很多时间阅读你可能不需要派生状态,但对那里提出的解决方案很不满意。

我知道这个问题的变体已经被问过多次,但我觉得任何答案都没有权衡可能的解决方案。 一些重复的例子:


附录

使用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
            }
        })
    }


}

正如我在评论中所说,我不喜欢这些解决方案。

组件不应该关心父级在做什么或父级的当前state是什么,它们应该简单地接收props并输出一些JSX ,这样它们才真正可重用、可组合和隔离,这也使测试更容易。

我们可以让NamesCarousel组件保存轮播的名称以及轮播的功能和当前可见的名称,并创建一个只做一件事的Name组件,显示通过props进来的名称

要在项目更改时重置selectedIndex ,请添加一个useEffect项目作为依赖项的useEffect ,但如果您只是将项目添加到数组的末尾,则可以忽略此部分

const Name = ({ name }) => <span>{name.toUpperCase()}</span>;

const NamesCarousel = ({ names }) => {
  const [selectedIndex, setSelectedIndex] = useState(0);

  useEffect(() => {
    setSelectedIndex(0)
  }, [names])// when names changes reset selectedIndex

  const next = () => {
    setSelectedIndex(prevIndex => prevIndex + 1);
  };

  const prev = () => {
    setSelectedIndex(prevIndex => prevIndex - 1);
  };

  return (
    <div>
      <button onClick={prev} disabled={selectedIndex === 0}>
        Prev
      </button>
      <Name name={names[selectedIndex]} />
      <button onClick={next} disabled={selectedIndex === names.length - 1}>
        Next
      </button>
    </div>
  );
};

现在这很好,但是NamesCarousel重用吗? 不, Name组件只是CarouselName组件耦合。

那么我们可以做些什么来使其真正可重用并看到孤立设计组件的好处呢?

我们可以利用渲染道具模式。

让我们做一个普通的Carousel组件,它会采取一个通用的列表items ,并调用children功能通过在选定的item

const Carousel = ({ items, children }) => {
  const [selectedIndex, setSelectedIndex] = useState(0);

  useEffect(() => {
    setSelectedIndex(0)
  }, [items])// when items changes reset selectedIndex

  const next = () => {
    setSelectedIndex(prevIndex => prevIndex + 1);
  };

  const prev = () => {
    setSelectedIndex(prevIndex => prevIndex - 1);
  };

  return (
    <div>
      <button onClick={prev} disabled={selectedIndex === 0}>
        Prev
      </button>
      {children(items[selectedIndex])}
      <button onClick={next} disabled={selectedIndex === items.length - 1}>
        Next
      </button>
    </div>
  );
};

现在这个模式实际上给了我们什么?

它使我们能够像这样渲染Carousel组件

// items can be an array of any shape you like
// and the children of the component will be a function 
// that will return the select item
<Carousel items={["Hi", "There", "Buddy"]}>
  {name => <Name name={name} />} // You can render any component here
</Carousel>

现在它们既是独立的又是真正可重用的,您可以将items作为图像、视频甚至用户的数组传递。

您可以更进一步,为轮播提供要作为道具显示的项目数量,并使用项目数组调用子函数

return (
  <div>
    {children(items.slice(selectedIndex, selectedIndex + props.numOfItems))}
  </div>
)

// And now you will get an array of 2 names when you render the component
<Carousel items={["Hi", "There", "Buddy"]} numOfItems={2}>
  {names => names.map(name => <Name key={name} name={name} />)}
</Carousel>

您可以使用功能组件吗? 可能会简化一些事情。

import React, { useState, useEffect } from "react";
import { Button } from "@material-ui/core";

interface Props {
    names: string[];
}

export const NameCarousel: React.FC<Props> = ({ names }) => {
  const [currentNameIndex, setCurrentNameIndex] = useState(0);

  const name = names[currentNameIndex].toUpperCase();

  useEffect(() => {
    setCurrentNameIndex(0);
  }, names);

  const handleButtonClick = () => {
    setCurrentIndex((currentNameIndex + 1) % names.length);
  }

  return (
    <div>
      {name}
      <Button onClick={handleButtonClick}>Next</Button>
    </div>
  )
};

useEffect类似于componentDidUpdate ,它将依赖项数组(状态和属性变量)作为第二个参数。 当这些变量发生变化时,将执行第一个参数中的函数。 就那么简单。 您可以在函数体内进行额外的逻辑检查以设置变量(例如, setCurrentNameIndex )。

如果您在函数内部更改的第二个参数中有依赖项,请小心,然后您将有无限的重新渲染。

查看useEffect 文档,但在习惯了钩子之后,您可能再也不想使用类组件了。

你问什么是最好的选择,最好的选择是让它成为受控组件。

组件在层次结构中太低,不知道如何处理它的属性更改 - 如果列表更改但只是轻微更改(可能添加新名称) - 调用组件可能希望保持原始位置。

在所有情况下,我都认为如果父组件可以决定组件在提供新列表时的行为方式,我们会更好。

也有可能这样的组件是更大整体的一部分,需要将当前选择传递给它的父级 - 也许作为表单的一部分。

如果您真的坚持不让它成为受控组件,还有其他选择:

  • 您可以将整个名称(或 id 组件)保留在状态中而不是索引 - 如果该名称不再存在于名称列表中,则返回列表中的第一个。 这与您的原始要求略有不同,对于一个非常非常长的列表可能是一个性能问题,但它非常干净。
  • 如果您对钩子没问题,那么 Asaf Aviv 建议的useEffect是一种非常干净的方法。
  • 使用类执行此操作的“规范”方法似乎是getDerivedStateFromProps - 是的,这意味着保留对状态中名称列表的引用并进行比较。 如果你这样写,它看起来会好一点:
   static getDerivedStateFromProps(props: Props, state: State = {}): Partial<State> {

       if( state.names !== props.names){
           return {
               currentNameIndex: 0,
               names: props.names
           }
       }

       return null; // you can return null to signify no change.
   }

(如果您选择这条路线,您可能也应该在渲染方法中使用state.names

但实际上 - 受控组件是要走的路,当需求发生变化并且父级需要知道所选项目时,您可能迟早会这样做。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM