ReactのonClickを介して要素のclassNameを変更すると、再レンダリングがトリガーされますが、条件付きclassNameの構築ではトリガーされませんか?

Aug 22 2020

私のアプリは、div要素の配列を使用してグリッドを表示しています。配列はContextに格納されるため、他のコンポーネントがデータにアクセスできますが、この場合にそれが重要かどうかはわかりません。クリックされた要素のCSSクラスを変更して、背景が色付きになるようにするonClick関数があります。これは正常に機能し、適切に切り替わります。

グリッドが小さいサイズに変更された場合、すべての選択を削除したいと思います。私はいくつかのアプローチを試しましたが、結果としていくつかの奇妙な動作が発生し、このコードから非常に紛らわしい動作になりました。

    selectTile(tilesArray, event) {
        event.preventDefault();
        let tempArray = [...tilesArray];
        if(event.target.className === "grid-tile"){
            event.target.className = "grid-tileb";
            tempArray.push(event.target.id);
        } else {
            event.target.className = "grid-tile";
            tempArray.splice(tempArray.indexOf(event.target.id),1);
        }
        this.setState({selectedTiles: tempArray})
    }
    makeGrid(x, y, tilesize, visible, hex){
        const gridsDataArray = JSON.parse(localStorage.getItem('grids'));
        const index = JSON.parse(localStorage.getItem('currentGrid'));
        let clearSelection = false;
        if(x < gridsDataArray[index].dims[0] || y < gridsDataArray[index].dims[1]){
            clearSelection = true;
        }
        let columnStr = "";
        let tileArray = [];
        const widthStr = tilesize.toString() + "px"
        if(clearSelection){
            this.setState({selectedTiles:[]})
        }
        for (let i = 0; i < y; i++) {
            for (let j = 0; j < x; j++) {
                if(i===0) columnStr = columnStr + "auto ";//x loops over columns so this runs once for all columns.
                let div = (
                    <div 
                        id={"x" + j.toString() + "y" + i.toString()}//for example at coordinates 5,6 id is x5y6.  starts at 0.
                        key={"x" + j.toString() + "y" + i.toString()}
                        className={(this.state.selectedTiles.indexOf("x" + j.toString() + "y" + i.toString()) < 0 ? 
                            "grid-tile" : 
                            "grid-tileb")}
                        style={{
                            width: widthStr,
                            height: widthStr,
                            border: "1px solid rgba(0, 0, 0," + (visible ? "0.6)" : "0.0)")
                        }}
                        onClick={(event) => this.selectTile(this.state.selectedTiles, event)}
                    >
                    </div>
                )
                tileArray.push(div);
            }
        }
        let iColumnStr = "";
        for (let i= 0; i < 330/tilesize; i++){
            iColumnStr = iColumnStr + "auto ";
        }
        return {
            columns: columnStr,
            imageColumns: iColumnStr,
            tiles: tileArray,
            name: gridsDataArray[index].name,
            bgurl: gridsDataArray[index].bgurl
        };
    }

いくつかのタイルを選択してからグリッドを小さくすると、以前に選択したタイルは色付きのままですが、selectedTiles配列は空です。何らかの方法でグリッドを再度変更すると、選択したタイルの色が失われます。さて、おそらくそれは、setStateが非同期であり、条件ステートメントが古い状態を使用しているためですよね?まあ、私はclassName={(clearSelection || this.state.selectedTiles.indexOf("x" + j.toString() + "y" + i.toString()) < 0 ? 代わりに試しましたが、それはうまくいきません。実際、動作は少なく、グリッドを2回変更した後でも、選択したタイルの色が失われることはありません。そのシナリオは私が置く場合にも起こります

        if(clearSelection){
            this.setState({selectedTiles:[]})
        }

forループの前ではなく、後。これは、非同期の問題でもないことを示しているようです。さらに、色付きの要素がグリッドの一部ではなくなるようにグリッドの寸法を縮小すると、それらは適切に削除され、予想どおりにグリッドを再び大きくすると、色なしの要素が追加されます。

要素が適切に再レンダリングされないのはなぜですか?そもそもselectTileが再レンダリングをトリガーする理由もわかりません。なぜなら、setState行を削除しても機能するからです。forループに条件付きのclassName構文も必要ありません。

makeGridを呼び出した後(または前)にselectedTiles配列全体でループを実行し、makeGrid内からではなく、その方法でクラス名を直接変更することで、この問題を完全に回避できると思いますが、なぜこの動作が発生するのかを知りたいです。 。

編集:selectedTiles配列に含まれる座標を介してタイル配列から取得することによってクラス名を変更することはできません(そうするためのコードは理解するのが楽しかったですが)。JSXオブジェクトは、直接操作されることを好みません。代わりに、selectedTiles全体でループを実行し、新しいグリッドの外側にある座標のみを削除しました。タイルがグリッドの外側にある場合、グリッドが再び大きくなると、とにかく完全にリセットされるため、残りのデータのみが選択されたタイルにあります。

回答

1 Terminat Aug 23 2020 at 15:07

さて、私はあなたのコードの問題をここで見つけたと思います。

できるだけ状態に依存しないようにしようとしましたが、それがここで問題を引き起こしました。

まず、グリッドメタデータを状態に含め、localStorageから開始値に割り当てて、グリッド内からアクセスできるようにしました。

    this.state = {
      x: topGridData.dims[0],
      y: topGridData.dims[1],
      tilesize: topGridData.dims[2],
      visible: topGridData.visible,
      hex: topGridData.hex,
      selectedTiles: [],
      ...this.makeGrid(topGridData.dims[0], topGridData.dims[1], topGridData.dims[2],
        topGridData.visible, topGridData.hex)
    };

次に、setGrid関数を変更して、グリッド値が変更されるとすぐに設定されるようにしました。

setGrid(x, y, tilesize, visible, hex) {
  this.setState({
    x,
    y,
    tilesize,
    visible,
    hex,
    ...this.makeGrid(x, y, tilesize, visible, hex)
  });
}

そして今、私たちは問題を引き起こしていた欠陥のある機能に行きます。この関数の問題は、Reactの外部でDOMを介してclassNameを設定していたため、Reactが要素の変更に適切に反応できなかった(しゃれが意図されていない)ことでした。

したがって、最初に、これら2つは不要なので、削除しました。

event.target.className = "grid-tileb";
event.target.className = "grid-tile";

次に、setStateにコールバックをアタッチして、selectedTiles配列が変更されるたびにグリッドが適切に再構築されるようにしました。

this.setState({
  selectedTiles: tempArray
}, () => this.setState(this.makeGrid(this.state.x, this.state.y, this.state.tilesize, this.state.visible, this.state.hex)));
そして、これはselectTile関数全体がどのように見えるかです:

selectTile(tilesArray, event) {
  event.preventDefault();
  let tempArray = [...tilesArray];
  if (event.target.className === "grid-tile") {
    tempArray.push(event.target.id);
  } else {
    tempArray.splice(tempArray.indexOf(event.target.id), 1);
  }
  this.setState({
    selectedTiles: tempArray
  }, () => this.setState(this.makeGrid(this.state.x, this.state.y, this.state.tilesize, this.state.visible, this.state.hex)))
}

ここで重要なので、状態のクリアを残しました。状態から選択されたタイルをクリアする必要があります

if (clearSelection) {
  this.setState({
    selectedTiles: []
  });
}

最後に追加したのは、元のコードから実際に取得された新しい条件です。

className = {
  (clearSelection || this.state.selectedTiles.indexOf("x" + j.toString() + "y" + i.toString()) < 0 ?
    "grid-tile" :
    "grid-tileb")
}

そしてそれがすべてです、それはあなたが最初に意図したように機能します!。