これらの戦略のどれが小道具が変化したときにコンポーネントの状態をリセットするための最良の方法です


8

テキストフィールドとボタンを備えた非常にシンプルなコンポーネントがあります。

<画像>

入力としてリストを受け取り、ユーザーがリストを循環できるようにします。

コンポーネントには次のコードがあります。

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メソッドに句「何もしない」を追加する必要があります。注意しないと、null-pointer-exceptionが発生する可能性があります。 。

これの実装を以下に含めました。

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


}

3
コンポーネントが単一の名前を表示しているだけなのに、なぜコンポーネントが名前のリスト全体を取得しているのですか?状態を上げて、選択した名前を小道具として渡し、を更新する関数を渡しcurrentIndexます。names親で更新されたときはリセットされるcurrentIndex
Asaf Aviv

1
@AsafAvivsソリューションが好きです:関数nextをコンポーネントに渡します。ボタンが表示名、およびそのonClickのは、ちょうどサイクリングを扱う親に位置し、次の関数呼び出し
ダニエル・ソモギ

1
@DánielSomogyi-私は親にも同様の問題があると思います……
sixtyfootersdude

1
名前を保持する親コンポーネントを含めることはできますか?
アサフアビブ

1
@DánielSomogyiは、問題を親に移動します。ただし、親が非同期フェッチを実行するコンポーネントで、状態をいつリセットするかを知ることができる場合は除きます。
エミールベルジェロン

回答:


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

useEffectcomponentDidUpdate2番目の引数として依存関係(状態変数とプロップ変数)の配列を取る場所に似ています。これらの変数が変更されると、最初の引数の関数が実行されます。そのような単純な。関数本体内で追加のロジックチェックを実行して、変数を設定できます(例:)setCurrentNameIndex

関数内で変更される2番目の引数に依存関係がある場合は、注意してください。無限の再レンダリングが発生します。

useEffect docsを確認してください。ただし、フックに慣れた後は、クラスコンポーネントを再び使用することはおそらくないでしょう。


これは良い答えです。のために動作するように見えuseEffectます。これが機能する理由を説明するために更新した場合、これを承認済みの回答としてマークします。
sixtyfootersdude

更新された回答を確認してください。さらに説明が必要な場合はお知らせください。
タン

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コンポーネントはですが、コンポーネントとCarousel結合されていNameます。

では、それを本当に再利用可能にして、コンポーネントを個別に設計するメリットを確認するにはどうすればよいでしょうか。

レンダープロップパターンを利用できます

Carousel一般的なリストを取り、選択されitemschildren関数を渡す関数を呼び出す一般的なコンポーネントを作成しましょう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>

selectedIndex小道具が変わってもリセットされるとは思いませんか?もしそうなら、どうですか?
sixtyfootersdude

1
@sixtyfootersdude現在いいえ、私は1秒以内に私の答えを更新します。
アサフアビブ

1

あなたは最良のオプションは何であるかを尋ねます、最良のオプションはそれを制御されたコンポーネントにすることです。

コンポーネントの階層が低すぎて、プロパティの変更を処理する方法を知ることができません-リストが変更されたが、ほんの少し(おそらく新しい名前が追加された場合)-呼び出し側のコンポーネントは、元の位置を維持したい場合があります。

すべての場合において、新しいリストが提供されたときにコンポーネントがどのように動作するかを親コンポーネントが決定できれば、私たちのほうが良いと考えることができます。

また、そのようなコンポーネントがより大きな全体の一部であり、現在の選択をその親に渡す必要がある可能性もあります-おそらくフォームの一部として。

それを制御されたコンポーネントにしないことに本当に強い場合は、他のオプションがあります:

  • インデックスの代わりに、名前全体(またはidコンポーネント)を状態に保持できます。その名前が名前リストに存在しない場合は、リストの最初を返します。これは元の要件とは少し異なる動作であり、本当に本当に長いリストのパフォーマンスの問題である可能性がありますが、非常にクリーンです。
  • あなたがフックで大丈夫ならuseEffect、Asaf Avivが示唆するよりも、それを行う非常にきれいな方法です。
  • クラスでそれを行う「標準的な」方法のようです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このルートを選択する場合は、おそらくrenderメソッドでも使用する必要があります)

しかし、実際には、制御されたコンポーネントが最適です。要求が変更され、親が選択されたアイテムを知る必要がある場合は、遅かれ早かれそれを実行するでしょう。


私は以下に同意しませんがThe component is too low in the hierarchy to know how to handle it's properties changing、これが最良の答えです。あなたが提供する選択肢は興味深く、有用です。この小道具を公開したくない主な理由は、このコンポーネントが複数のプロジェクトで使用され、APIを最小限にしたいということです。
sixtyfootersdude

ただし、1つの注意:If you are ok with hooks, than useEffect as Asaf Aviv suggested is a very clean way to do it.-これは問題を解決しません-4票の投票があるにもかかわらず、その質問は小道具変更の状態をリセットしません。
sixtyfootersdude

useEffect(() => { setSelectedIndex(0) }, [items]) UseEffectの2番目の引数は、いつ実行するかを定義しsetSelectedIndex(0)ます。したがって、項目が変更されるたびに効果的に実行されます。私が理解している限り、これはあなたの要件です。
Alon Bar David

@sixtyfootersdudeはuseEffect、私のソリューションではnames変更時にインデックスをリセットし、Tunn useEffectは無効であり、リンティングエラーが表示され、名前が変更されるとエラーがスローされます。
アサフアビブ

それがあなたのためにリセットされない場合、あなたはnames直接変異しており、これは別の問題であることを言及する価値があります。
アサフアビブ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.