Welche dieser Strategien ist der beste Weg, um den Status einer Komponente zurückzusetzen, wenn sich die Requisiten ändern

8

Ich habe eine sehr einfache Komponente mit einem Textfeld und einer Schaltfläche:

<Bild>

Es nimmt eine Liste als Eingabe und ermöglicht dem Benutzer, durch die Liste zu blättern.

Die Komponente hat den folgenden Code:

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

Diese Komponente funktioniert hervorragend, außer ich habe den Fall nicht behandelt, wenn sich der Status ändert. Wenn sich der Zustand ändert, möchte ich den currentNameIndexauf Null zurücksetzen .

Was ist der beste Weg, dies zu tun?


Optionen, die ich berücksichtigt habe:

Verwenden von componentDidUpdate

Diese Lösung ist rückständig, da sie componentDidUpdatenach dem Rendern ausgeführt wird. Daher muss ich in der Rendermethode eine Klausel hinzufügen, um "nichts zu tun", während sich die Komponente in einem ungültigen Zustand befindet. Wenn ich nicht aufpasse, kann ich eine Nullzeigerausnahme verursachen .

Ich habe unten eine Implementierung davon aufgenommen.

Verwenden von getDerivedStateFromProps

Die getDerivedStateFromPropsMethode ist staticund die Signatur gibt Ihnen nur Zugriff auf den aktuellen Status und die nächsten Requisiten. Dies ist ein Problem, da Sie nicht feststellen können, ob sich die Requisiten geändert haben. Infolgedessen müssen Sie die Requisiten in den Status kopieren, damit Sie überprüfen können, ob sie identisch sind.

Die Komponente "vollständig gesteuert" machen

Ich will das nicht machen. Diese Komponente sollte privat den aktuell ausgewählten Index besitzen.

Die Komponente "mit einem Schlüssel völlig unkontrolliert" machen

Ich denke über diesen Ansatz nach, mag aber nicht, wie der Elternteil die Implementierungsdetails des Kindes verstehen muss.

Verknüpfung


Sonstiges

Ich habe viel Zeit damit verbracht, zu lesen, dass Sie wahrscheinlich keinen abgeleiteten Zustand benötigen , bin aber mit den dort vorgeschlagenen Lösungen weitgehend unzufrieden.

Ich weiß, dass Variationen dieser Frage mehrfach gestellt wurden, aber ich habe nicht das Gefühl, dass eine der Antworten die möglichen Lösungen abwägt. Einige Beispiele für Duplikate:


Blinddarm

Lösung mit componetDidUpdate(siehe Beschreibung oben)

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

}

Lösung mit 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
            }
        })
    }


}
sechzig Fuß
quelle
3
Warum erhält die Komponente die gesamte Liste der Namen, wenn ihr Job nur einen einzelnen Namen anzeigt? Heben Sie den Status an und übergeben Sie den ausgewählten Namen einfach als Requisite und als Funktion zum Aktualisieren des currentIndex. Wenn namesim übergeordneten Element aktualisiert werden, einfach zurücksetzencurrentIndex
Asaf Aviv
1
Ich mag die @ AsafAvivs-Lösung: Übergebe eine Funktion nextan die Komponente. Die Schaltfläche zeigt den Namen an und ihr onClick ruft nur die nächste Funktion auf, die sich im übergeordneten Element befindet und das Radfahren übernimmt
Dániel Somogyi,
1
@ DánielSomogyi - Ich denke, ich werde ein ähnliches Problem bei den Eltern haben ......
Sixtyfootersdude
1
Können Sie die übergeordnete Komponente einschließen, die die Namen enthält?
Asaf Aviv
1
@ DánielSomogyi, der das Problem nur auf das übergeordnete Element verschiebt, es sei denn, das übergeordnete Element ist die Komponente, die das asynchrone Abrufen ausführt und weiß, wann der Status zurückgesetzt werden muss.
Emile Bergeron

Antworten:

2

Können Sie eine Funktionskomponente verwenden? Könnte die Dinge ein bisschen vereinfachen.

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ähnelt dem Ort, an componentDidUpdatedem ein Array von Abhängigkeiten (Status- und Prop-Variablen) als zweites Argument verwendet wird. Wenn sich diese Variablen ändern, wird die Funktion im ersten Argument ausgeführt. So einfach ist das. Sie können zusätzliche Logikprüfungen innerhalb des Funktionskörpers durchführen, um Variablen festzulegen (z setCurrentNameIndex. B. ).

Seien Sie nur vorsichtig, wenn Sie eine Abhängigkeit im zweiten Argument haben, die innerhalb der Funktion geändert wird, dann haben Sie unendlich viele Renderings.

Schauen Sie sich die useEffect-Dokumente an , aber Sie werden wahrscheinlich nie wieder eine Klassenkomponente verwenden wollen, nachdem Sie sich an Hooks gewöhnt haben .

Tunn
quelle
Das ist eine gute Antwort. Sieht aus wie es aufgrund der funktioniert useEffect. Wenn Sie aktualisieren, um zu erklären, warum dies funktioniert, werde ich dies als akzeptierte Antwort markieren.
Sixtyfootersdude
Schauen Sie sich die aktualisierte Antwort an und lassen Sie mich wissen, wenn Sie weitere Erläuterungen benötigen.
Tunn
4

Wie ich in den Kommentaren sagte, bin ich kein Fan dieser Lösungen.

Komponenten sollten sich nicht darum kümmern, was der Elternteil tut oder was der Strom statedes Elternteils ist. Sie sollten einfach propseinige aufnehmen und ausgeben JSX. Auf diese Weise sind sie wirklich wiederverwendbar, zusammensetzbar und isoliert, was auch das Testen erheblich erleichtert.

Wir können die NamesCarouselKomponente dazu bringen, die Namen des Karussells zusammen mit der Funktionalität des Karussells und dem aktuell sichtbaren Namen Namezu speichern, und eine Komponente erstellen, die nur eines tut: den Namen anzeigen, der durchkommtprops

Um das Zurücksetzen der selectedIndexElemente zurückzusetzen, fügen Sie ein useEffectmit Elementen als Abhängigkeit hinzu. Wenn Sie jedoch nur Elemente am Ende des Arrays hinzufügen, können Sie diesen Teil ignorieren

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

Nun ist das in Ordnung, aber ist das NamesCarouselwiederverwendbar? Nein, die NameKomponente ist aber die Carouselist mit der NameKomponente gekoppelt .

Was können wir also tun, um es wirklich wiederverwendbar zu machen und die Vorteile des isolierten Entwurfs von Komponenten zu sehen ?

Wir können das Render-Requisitenmuster nutzen .

Lassen Sie Carouseluns eine generische Komponente erstellen, die eine generische Liste von itemsund die childrenFunktion aufruft, die in der ausgewählten übergeben wirditem

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

Was gibt uns dieses Muster eigentlich?

Es gibt uns die Möglichkeit, die CarouselKomponente so zu rendern

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

Jetzt sind sie beide isoliert und wirklich wiederverwendbar. Sie können sie itemsals eine Reihe von Bildern, Videos oder sogar Benutzern übergeben.

Sie können noch weiter gehen und dem Karussell die Anzahl der Elemente geben, die Sie als Requisiten anzeigen möchten, und die untergeordnete Funktion mit einer Reihe von Elementen aufrufen

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>
Asaf Aviv
quelle
Ich glaube nicht, dass dies zurückgesetzt wird, selectedIndexwenn sich die Requisiten ändern, oder? Wenn das so ist, wie?
Sixtyfootersdude
1
@sixtyfootersdude derzeit nein, ich werde meine Antwort in einer Sekunde aktualisieren.
Asaf Aviv
1

Sie fragen, was die beste Option ist. Die beste Option besteht darin, sie zu einer kontrollierten Komponente zu machen.

Die Komponente ist in der Hierarchie zu niedrig, um zu wissen, wie mit sich ändernden Eigenschaften umgegangen werden soll. Wenn sich die Liste nur geringfügig ändert (möglicherweise wird ein neuer Name hinzugefügt), möchte die aufrufende Komponente möglicherweise die ursprüngliche Position beibehalten.

In allen Fällen kann ich mir vorstellen, dass es uns besser geht, wenn die übergeordnete Komponente entscheiden kann, wie sich die Komponente verhalten soll, wenn eine neue Liste bereitgestellt wird.

Es ist auch wahrscheinlich, dass eine solche Komponente Teil eines größeren Ganzen ist und die aktuelle Auswahl an das übergeordnete Element übergeben muss - möglicherweise als Teil eines Formulars.

Wenn Sie wirklich darauf aus sind, es nicht zu einer kontrollierten Komponente zu machen, gibt es andere Möglichkeiten:

  • Anstelle eines Index können Sie den gesamten Namen (oder eine ID-Komponente) im Status behalten - und wenn dieser Name nicht mehr in der Namensliste vorhanden ist, geben Sie den ersten in der Liste zurück. Dies ist ein etwas anderes Verhalten als Ihre ursprünglichen Anforderungen und möglicherweise ein Leistungsproblem für eine wirklich sehr lange Liste, aber es ist sehr sauber.
  • Wenn Sie mit Haken useEffecteinverstanden sind, ist dies , wie Asaf Aviv vorgeschlagen hat, eine sehr saubere Methode.
  • Die "kanonische" Art, dies mit Klassen zu tun, scheint zu sein getDerivedStateFromProps- und ja, das bedeutet, einen Verweis auf die Namensliste im Status zu behalten und sie zu vergleichen. Es kann ein bisschen besser aussehen, wenn Sie es so schreiben:
   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.
   }

(Sie sollten wahrscheinlich auch state.namesdie Rendermethode verwenden, wenn Sie diese Route wählen.)

Aber eine wirklich kontrollierte Komponente ist der richtige Weg. Sie werden es wahrscheinlich sowieso früher oder später tun, wenn sich die Anforderungen ändern und der Elternteil das ausgewählte Element kennen muss.

Alon Bar David
quelle
Obwohl ich nicht einverstanden bin mit: The component is too low in the hierarchy to know how to handle it's properties changingist dies die beste Antwort. Die von Ihnen angebotenen Alternativen sind interessant und nützlich. Der Hauptgrund, warum ich diese Requisite nicht verfügbar machen möchte, ist, dass diese Komponente in mehreren Projekten verwendet wird und die API minimal sein soll.
Sixtyfootersdude
Ein Hinweis: If you are ok with hooks, than useEffect as Asaf Aviv suggested is a very clean way to do it.- Dies löst das Problem nicht - trotz 4 Stimmen setzt diese Frage den Status beim Requisitenwechsel nicht zurück.
Sixtyfootersdude
useEffect(() => { setSelectedIndex(0) }, [items]) Das zweite Argument von UseEffect definiert, wann es ausgeführt werden soll - so effektiv, dass es ausgeführt wird, setSelectedIndex(0)wenn sich Elemente ändern. Welches ist Ihre Anforderung, soweit ich es verstehe.
Alon Bar David
@sixtyfootersdude das useEffectin meiner Lösung setzt den Index zurück, wenn namesÄnderungen vorgenommen werden, Tunn's useEffectist ungültig und zeigt Flusenfehler und wirft Fehler aus, wenn sich Namen ändern.
Asaf Aviv
Erwähnenswert ist, dass Sie namesdirekt mutieren, wenn es für Sie nicht zurückgesetzt wird. Dies ist ein weiteres Problem.
Asaf Aviv