道具更改时如何重置组件状态

时间:2019-12-19 22:18:49

标签: javascript reactjs typescript

我有一个非常简单的组件,看起来像这样:

<image>

它将列表作为输入,并允许用户在列表中循环。

该组件具有以下代码:

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

此解决方案比较棘手,因为componentDidUpdate在渲染后运行,因此我需要添加一个子句 在组件处于无效状态时,可以使用render方法“不执行任何操作”,如果我不小心的话, 我可以导致一个空指针异常。

我在下面包含了此实现。

使用getDerivedStateFromProps

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

使组件“完全受控”

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

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

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

Link


其他

我花了大量时间阅读You Probably Don't Need Derived State 但对那里提出的解决方案不满意。

我知道这个问题的变化已经被问过多次了,但是我不认为任何答案都会权衡可能的解决方案。重复的一些示例:


附录

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


}

3 个答案:

答案 0 :(得分:4)

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

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

我们可以使NamesCarousel组件包含轮播的名称以及该轮播的功能和当前的可见名称,并制作一个Name组件,该组件仅做一件事,显示该名称通过props

进入

要在更改项目时重置selectedIndex,请添加一个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组件耦合。

那么我们该怎么做才能使其真正可重用并看到单独设计组件的好处

我们可以利用render props模式。

让我们制作一个通用的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>

答案 1 :(得分:2)

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

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>
  )
};

useEffectcomponentDidUpdate相似,其中它将一个依赖项数组(状态变量和prop变量)作为第二个参数。当这些变量更改时,将执行第一个参数中的函数。就那么简单。您可以在函数体内进行其他逻辑检查以设置变量(例如setCurrentNameIndex)。

请小心,如果在函数内部更改了第二个参数的依赖项,那么您将拥有无限的回报。

签出useEffect docs,但是在习惯了钩子之后,您可能永远不想再使用类组件。

答案 2 :(得分:1)

您问什么是最好的选择,最好的选择是使其成为受控组件。

该组件的层次结构太低,以至于不知道如何处理其属性更改-如果列表更改了但只是稍稍更改(也许添加了新名称)该怎么办-调用组件可能想要保持原始位置。

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

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

如果您真的不希望将其设为受控组件,则还有其他选择:

  • 您可以将整个名称(或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.
   }

(如果选择此路线,您可能还应该在render方法中使用state.names

但实际上-受控制的组件是必经之路,您可能迟早会在​​需求发生变化且父项需要知道所选项目的情况下这样做。