La modifica del nome di classe di un elemento tramite onClick in React attiva un nuovo rendering, ma la costruzione condizionale di nome di classe no?
La mia app utilizza una matrice di elementi div per visualizzare una griglia. L'array è memorizzato in Context in modo che altri componenti possano accedere ai dati, ma non sono sicuro che sia importante in questo caso. Ho una funzione onClick che cambia la classe CSS di un elemento cliccato, in modo che abbia uno sfondo colorato. Funziona bene e si attiva correttamente.
Voglio rimuovere tutte le selezioni se la griglia viene modificata in una dimensione più piccola. Ho provato alcuni approcci e di conseguenza ho ottenuto uno strano comportamento, culminato in un comportamento molto confuso da questo codice:
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
};
}
Se seleziono alcune tessere, quindi rimpicciolisco la griglia, le tessere precedentemente selezionate rimangono colorate, ma l'array selectedTiles è vuoto. Se cambio di nuovo la griglia in qualsiasi modo, le tessere selezionate perdono la colorazione. Quindi va bene, forse è perché setState è asincrono e l'istruzione condizionale utilizza il vecchio stato, giusto? Bene, ho provato className={(clearSelection || this.state.selectedTiles.indexOf("x" + j.toString() + "y" + i.toString()) < 0 ?
invece e non funziona. In effetti, funziona MENO: le tessere selezionate non perdono mai colore anche dopo aver cambiato la griglia due volte. Questo scenario si verifica anche se metto
if(clearSelection){
this.setState({selectedTiles:[]})
}
dopo il ciclo for invece che prima di esso. Ciò sembra indicare che NON si tratta anche di un problema asincrono. Inoltre, se riduco le dimensioni della griglia in modo che gli elementi colorati non facciano più parte della griglia, vengono rimossi correttamente e gli elementi non colorati vengono aggiunti una volta che avrò ingrandito nuovamente la griglia come ti aspetteresti.
Perché i miei elementi non vengono sottoposti a rendering correttamente? Non sono nemmeno sicuro del motivo per cui selectTile attiva un nuovo rendering in primo luogo, poiché se rimuovo la riga setState da esso, funziona ancora. Non ho nemmeno bisogno di alcuna costruzione className condizionale nel ciclo for.
Penso di poter aggirare completamente questo problema eseguendo un ciclo sull'array selectedTiles dopo (o prima) che chiamo makeGrid e cambiando direttamente i nomi delle classi in quel modo invece che dall'interno di makeGrid, ma mi piacerebbe comunque sapere perché si verifica questo comportamento .
modifica: non è possibile modificare i nomi delle classi recuperando dall'array tile tramite le coordinate contenute nell'array selectedTiles (sebbene il codice per farlo fosse divertente da capire); Gli oggetti JSX non amano essere manipolati direttamente. Invece, ho eseguito un ciclo tra le tessere selezionate e ho rimosso solo le coordinate che erano al di fuori della nuova griglia. Se i riquadri sono al di fuori della griglia, vengono comunque reimpostati completamente quando la griglia viene nuovamente ingrandita, quindi gli unici dati rimanenti sono in selezionatoTiles.
Risposte
Ok, penso di aver trovato il problema con il tuo codice qui.
L'hai provato per renderlo il più indipendente possibile dallo stato e questo ha causato il problema qui.
In primo luogo, ho incluso i metadati della griglia da dichiarare e li ho assegnati ai valori iniziali da localStorage, in modo che sia accessibile dall'interno della griglia
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)
};
Quindi ho modificato la tua funzione setGrid, in modo che imposti i valori della griglia non appena cambiano
setGrid(x, y, tilesize, visible, hex) {
this.setState({
x,
y,
tilesize,
visible,
hex,
...this.makeGrid(x, y, tilesize, visible, hex)
});
}
E ora cerchiamo la funzione difettosa che causava il problema. Il problema con questa funzione era che stava impostando className tramite il DOM, al di fuori di React, quindi React non era in grado di reagire (gioco di parole non intenzionale) correttamente alla modifica dell'elemento.
Quindi in primo luogo mi sono sbarazzato di questi due in quanto non sono necessari:
event.target.className = "grid-tileb";
event.target.className = "grid-tile";
Quindi ho allegato un callback a un setState, in modo che Grid venga ricostruito correttamente, ogni volta che l'array selectedTiles viene modificato
this.setState({
selectedTiles: tempArray
}, () => this.setState(this.makeGrid(this.state.x, this.state.y, this.state.tilesize, this.state.visible, this.state.hex)));
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)))
}
Ho lasciato la tua cancellazione dello stato, perché è importante qui, le tessere selezionate dallo stato devono essere cancellate
if (clearSelection) {
this.setState({
selectedTiles: []
});
}
L'ultima cosa che ho aggiunto è una nuova condizione che è effettivamente presa dal tuo codice originale
className = {
(clearSelection || this.state.selectedTiles.indexOf("x" + j.toString() + "y" + i.toString()) < 0 ?
"grid-tile" :
"grid-tileb")
}
E questo è tutto, funziona come inizialmente previsto!.