代码之家  ›  专栏  ›  技术社区  ›  sixtyfootersdude

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

  •  0
  • sixtyfootersdude  · 技术社区  · 6 年前

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

    <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

    这个解决方案是背道而驰的,因为 在render之后运行,所以我需要添加一个子句 当组件处于无效状态时,在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
                }
            })
        }
    
    
    }
    
    0 回复  |  直到 6 年前
        1
  •  2
  •   Tunn    6 年前

    你能使用功能部件吗?可能会简化一点。

    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 docs

        2
  •  4
  •   Asaf Aviv    6 年前

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

    组件不应该关心父级正在做什么或当前正在做什么 state 对于父母,他们应该简单地接受 props JSX ,这样它们才是真正可重用的、可组合的和独立的,这也使得测试更加容易。

    我们可以 NamesCarousel 组件将旋转木马的名称、旋转木马的功能和当前可见的名称放在一起,并生成一个 Name 道具

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

    这很好,但是 可重复使用的?不,那个 组件只是 Carousel 与 姓名 组件。

    那么,我们该怎么做才能使它真正地可重用,并看到独立设计组件的好处呢 ?

    render props 图案。

    让我们做一个通用的 组件,它将获取 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>
      );
    };
    

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

    它使我们能够

    // 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>
    

    现在它们都是独立的,而且是真正可重用的,你可以通过了 作为图像、视频甚至用户的数组。

    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>
    
        3
  •  1
  •   Alon Bar David    6 年前

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

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

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

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

    • 您可以将整个名称(或id组件)保持在状态,而不是索引,如果名称列表中不再存在该名称,则返回列表中的第一个名称。这与您最初的需求稍有不同,对于一个非常长的列表来说,这可能是一个性能问题,但它非常清晰。
    • 如果你对钩子没问题的话 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

    但真正控制的组件是一种方法,你可能迟早都会这样做,当需求发生变化时,父对象需要知道所选的项。