Chiến lược nào trong số này là cách tốt nhất để đặt lại trạng thái của thành phần khi đạo cụ thay đổi


8

Tôi có một thành phần rất đơn giản với trường văn bản và nút:

<hình ảnh>

Nó lấy một danh sách làm đầu vào và cho phép người dùng chuyển qua danh sách.

Thành phần có mã sau:

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

Thành phần này hoạt động rất tốt, ngoại trừ tôi đã không xử lý trường hợp khi trạng thái thay đổi. Khi trạng thái thay đổi, tôi muốn đặt lại currentNameIndexvề không.

Cách tốt nhất để làm việc này là gì?


Các tùy chọn tôi đã xem xét:

Sử dụng componentDidUpdate

Giải pháp này rất khó hiểu, vì componentDidUpdatechạy sau khi kết xuất, vì vậy tôi cần thêm một mệnh đề trong phương thức kết xuất thành "không làm gì" trong khi thành phần ở trạng thái không hợp lệ, nếu tôi không cẩn thận, tôi có thể gây ra ngoại lệ con trỏ null .

Tôi đã bao gồm một thực hiện này dưới đây.

Sử dụng getDerivedStateFromProps

Các getDerivedStateFromPropsphương pháp là staticvà chữ ký chỉ cung cấp cho bạn truy cập vào các đạo cụ nhà nước và tiếp theo hiện hành. Đây là một vấn đề bởi vì bạn không thể biết nếu các đạo cụ đã thay đổi. Kết quả là, điều này buộc bạn phải sao chép các đạo cụ vào trạng thái để bạn có thể kiểm tra xem chúng có giống nhau không.

Làm cho thành phần "được kiểm soát hoàn toàn"

Tôi không muốn làm điều này. Thành phần này nên sở hữu riêng những gì chỉ số hiện được chọn.

Làm cho thành phần "hoàn toàn không được kiểm soát với một khóa"

Tôi đang xem xét phương pháp này, nhưng không thích cách nó khiến phụ huynh cần hiểu chi tiết thực hiện của trẻ.

Liên kết


Linh tinh

Tôi đã dành rất nhiều thời gian để đọc Bạn có lẽ không cần Nhà nước xuất phát nhưng phần lớn không hài lòng với các giải pháp được đề xuất ở đó.

Tôi biết rằng các biến thể của câu hỏi này đã được hỏi nhiều lần, nhưng tôi không cảm thấy như bất kỳ câu trả lời nào cân nhắc các giải pháp có thể. Một số ví dụ về các bản sao:


ruột thừa

Giải pháp sử dụng componetDidUpdate(xem mô tả ở trên)

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

}

Giải pháp sử dụng 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
Tại sao thành phần nhận được toàn bộ danh sách các tên khi công việc của nó chỉ hiển thị một tên duy nhất? nâng trạng thái lên và chỉ cần chuyển tên đã chọn làm chỗ dựa và chức năng để cập nhật currentIndex. Khi namesđang được cập nhật, phụ huynh chỉ cần đặt lạicurrentIndex
Asaf Aviv

1
Tôi thích giải pháp @AsafAvivs: truyền một hàm nextcho thành phần. Nút hiển thị tên và onClick của nó chỉ gọi chức năng tiếp theo, nằm trong phần cha mẹ, xử lý việc đạp xe
Dániel Somogyi

1
@ DánielSomogyi - Tôi nghĩ rằng tôi sẽ gặp vấn đề tương tự ở phụ huynh ......
Sixty feetersdude

1
Bạn có thể bao gồm các thành phần cha mẹ giữ tên?
Asaf Aviv

1
@ DánielSomogyi chỉ chuyển vấn đề sang cha mẹ, trừ khi cha mẹ là thành phần thực hiện tìm nạp async có thể biết khi nào cần đặt lại trạng thái.
Emile Bergeron

Câu trả lời:


2

Bạn có thể sử dụng một thành phần chức năng? Có thể đơn giản hóa mọi thứ một chút.

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

useEffecttương tự như componentDidUpdatenơi nó sẽ lấy một mảng các phụ thuộc (biến trạng thái và biến prop) làm đối số thứ hai. Khi các biến đó thay đổi, hàm trong đối số đầu tiên được thực thi. Đơn giản như thế. Bạn có thể thực hiện các kiểm tra logic bổ sung bên trong thân hàm để đặt các biến (ví dụ setCurrentNameIndex:).

Chỉ cần cẩn thận nếu bạn có một phụ thuộc trong đối số thứ hai được thay đổi bên trong hàm, thì bạn sẽ có các rerenders vô hạn.

Kiểm tra tài liệu useEffect , nhưng có lẽ bạn sẽ không bao giờ muốn sử dụng lại thành phần lớp sau khi đã quen với hook.


Đây là một câu trả lời tốt. Có vẻ như nó hoạt động do useEffect. Nếu bạn cập nhật để giải thích lý do tại sao điều này hoạt động, tôi sẽ đánh dấu đây là câu trả lời được chấp nhận.
Sixty feetersdude

Kiểm tra câu trả lời cập nhật, cho tôi biết nếu bạn cần làm rõ hơn.
Tunn

4

Như tôi đã nói trong các bình luận, tôi không phải là người hâm mộ các giải pháp này.

Các thành phần không nên quan tâm cha mẹ đang làm gì hoặc dòng điện statecủa cha mẹ là gì, họ chỉ nên tiếp nhận propsvà xuất ra một số JSX, theo cách này chúng thực sự có thể tái sử dụng, có thể ghép lại và cách ly cũng giúp việc kiểm tra dễ dàng hơn rất nhiều.

Chúng ta có thể làm cho NamesCarouselthành phần giữ tên của băng chuyền cùng với chức năng của băng chuyền và tên hiển thị hiện tại và tạo Namethành một thành phần chỉ có một thứ, hiển thị tên đi quaprops

Để đặt lại selectedIndexkhi các mục đang thay đổi, hãy thêm a useEffectvới các mục làm phụ thuộc, mặc dù nếu bạn chỉ thêm các mục vào cuối mảng, bạn có thể bỏ qua phần này

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

Bây giờ điều này là tốt nhưng là NamesCarouseltái sử dụng? không, Namethành phần là nhưng Carouselđược kết hợp với Namethành phần.

Vậy chúng ta có thể làm gì để làm cho nó thực sự có thể tái sử dụng và thấy được lợi ích của việc thiết kế thành phần một cách cô lập ?

Chúng ta có thể tận dụng mô hình đạo cụ render .

Cho phép tạo một Carouselthành phần chung sẽ lấy một danh sách chung itemsvà gọi childrenhàm đi qua trong phần được chọnitem

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

Bây giờ những gì mô hình này thực sự mang lại cho chúng ta?

Nó cho chúng ta khả năng kết xuất Carouselthành phần như thế này

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

Bây giờ chúng đều bị cô lập và thực sự có thể tái sử dụng, bạn có thể vượt qua itemsdưới dạng một loạt các hình ảnh, video hoặc thậm chí là người dùng.

Bạn có thể đưa nó đi xa hơn và cung cấp cho băng chuyền số lượng vật phẩm bạn muốn hiển thị làm đạo cụ và gọi hàm con với một loạt các vật phẩm

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>

Tôi không nghĩ rằng điều này sẽ thiết lập lại selectedIndexkhi đạo cụ thay đổi, phải không? Nếu vậy thì thế nào?
Sixty feetersdude

1
@sixtyfootersdude hiện không có, tôi sẽ cập nhật câu trả lời của tôi trong giây lát.
Asaf Aviv

1

Bạn hỏi tùy chọn tốt nhất là gì, tùy chọn tốt nhất là biến nó thành thành phần được Kiểm soát.

Thành phần này quá thấp trong cấu trúc phân cấp để biết cách xử lý các thuộc tính của nó thay đổi - điều gì sẽ xảy ra nếu danh sách thay đổi nhưng chỉ một chút (có thể thêm tên mới) - thành phần gọi có thể muốn giữ vị trí ban đầu.

Trong mọi trường hợp tôi có thể nghĩ về việc chúng ta sẽ tốt hơn nếu thành phần cha mẹ có thể quyết định cách thành phần đó hoạt động khi được cung cấp một danh sách mới.

Cũng có khả năng một thành phần như vậy là một phần của tổng thể lớn hơn và cần chuyển lựa chọn hiện tại cho cha mẹ của nó - có lẽ là một phần của biểu mẫu.

Nếu bạn thực sự kiên quyết không biến nó thành một thành phần được kiểm soát, có những lựa chọn khác:

  • Thay vì chỉ mục, bạn có thể giữ toàn bộ tên (hoặc thành phần id) ở trạng thái - và nếu tên đó không còn tồn tại trong danh sách tên, hãy trả về tên đầu tiên trong danh sách. Đây là một hành vi hơi khác so với yêu cầu ban đầu của bạn và có thể là một vấn đề hiệu suất cho một danh sách thực sự thực sự thực sự dài, nhưng nó rất sạch sẽ.
  • Nếu bạn ổn với móc, hơn useEffectnhư Asaf Aviv đề xuất là một cách rất sạch sẽ để làm điều đó.
  • Cách "chính tắc" để làm điều đó với các lớp dường như là getDerivedStateFromProps- và vâng, điều đó có nghĩa là giữ một tham chiếu đến danh sách tên trong trạng thái và so sánh nó. Nó có thể trông tốt hơn một chút nếu bạn viết nó như thế này:
   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.
   }

(có lẽ bạn cũng nên sử dụng state.namesphương thức kết xuất nếu bạn chọn tuyến đường này)

Nhưng thành phần thực sự được kiểm soát là con đường để đi, dù sao thì bạn cũng sẽ sớm thực hiện nó khi nhu cầu thay đổi và phụ huynh cần biết mặt hàng đã chọn.


Trong khi tôi không đồng ý với : The component is too low in the hierarchy to know how to handle it's properties changing, đây là câu trả lời tốt nhất. Các lựa chọn thay thế mà bạn cung cấp rất thú vị và hữu ích. Lý do chính tôi không muốn tiết lộ prop này là thành phần này sẽ được sử dụng trong nhiều dự án và tôi muốn API ở mức tối thiểu.
Sixty feetersdude

Tuy nhiên, một lưu ý: If you are ok with hooks, than useEffect as Asaf Aviv suggested is a very clean way to do it.- Điều này không giải quyết được vấn đề - mặc dù có 4 phiếu, câu hỏi đó không đặt lại trạng thái thay đổi đạo cụ.
Sixty feetersdude

useEffect(() => { setSelectedIndex(0) }, [items]) Đối số thứ hai của UseEffect xác định khi nào nên chạy - vì vậy, nó sẽ chạy hiệu quả setSelectedIndex(0)bất cứ khi nào các mục thay đổi. Đó là yêu cầu của bạn theo như tôi hiểu nó.
Alon Bar David

@sixtyfootersdude useEffecttrong giải pháp của tôi sẽ đặt lại chỉ mục khi namesthay đổi, Tunn useEffectkhông hợp lệ và sẽ hiển thị lỗi linting và sẽ ném lỗi khi tên thay đổi.
Asaf Aviv

Đáng nói là nếu nó không thiết lập lại cho bạn, bạn đang biến đổi namestrực tiếp thì đó là một vấn đề khác.
Asaf Aviv
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.