React oraz tablice wielowymiarowe w useState

0

Hej, staram sie napisac prosta gre typu candy crush. Chcialbym stworzyc sobie tablice ktora mialaby w sobie wiersze i kolumny a wiec pisze cos w stylu:

import { useState } from 'react';
import GridObject from './GridObject';


const colSize:number = 10;
const rowSize:number = 10;

type  GridObject = {
    color:string,
    rowIndex:number,
    colIndex:number
}

function GameMap() {
   const [gridsArray, setGridsArray] = useState([]);

   const generateGrid = () =>
   {

    let gridArray:Array<Array<GridObject>> = [];

    for(let i = 0; i < rowSize; i++)
    {
        let rowArr = [];

        for(let j = 0; j < colSize; j++)
        {
            rowArr.push(new GridObject());
        }
        console.log("dupa")
        gridArray.push(rowArr);
    }

    setGridsArray(gridArray);

    gridArray.map((el) => {
        console.log(el);
    })
    
   }

    return(
        <div>
         
        </div>
    )
}

export default GameMap;

Wywala blad:

Argument of type 'GridObject[][]' is not assignable to parameter of type 'SetStateAction<never[]>'.
  Type 'GridObject[][]' is not assignable to type 'never[]'.
    Type 'GridObject[]' is not assignable to type 'never'.ts(2345)

Rozumiem ze nie moge do usestate przypisac tablicy ktora ma w sobie inne tablice, jest na to jakies rozwiazanie?
Poza tym Wyobrazam sobie GridObject jako poszczegolna kratke, ktora ma konkretny wiersz oraz kolumne.

Chcialbym zeby GridObject byl standardowa klasa ktora ma w sobie wszystkie informacje w stylu stanu, grafiki, wymiarow itd. Tylko jak niby mialbym tworzyc instancje owej klasy w react, poniewaz chyba nie moge po prostu napisac rowArr.push(new GridObject());

Moge pisac standardowe klasy czy raczej nie tedy droga i powinienem konkretna kratke napisac jako osobny component cos w stylu:

const GridObject = () => {

return(
    <div className="grid">
    </div>
)

}
export default GridObject;

i wrzucac tam parametry w formie props?

Generalnie mozna w react mieszac standardowe klasy z construktorem itd. z reactem czy raczej tego unikac i o wszystkim myslec jak o componentach?

2

A weź spróbuj dodać jawnie typ:

const [gridsArray, setGridsArray] = useState<GridObject[][]>([]);

No i funkcja generateGrid. Nigdzie jej nie wywołujesz. Domyślam się, że dlatego, że nie zdążyłeś dopisać kodu, bo ci się pojawił błąd kompilacji?

Ale tak czy siak umieszczanie tej funkcji w komponencie nie ma sensu. Raczej wydzieliłbym ją poza komponent:


   const generateGrid = () => {

    let gridArray:Array<Array<GridObject>> = [];

    for(let i = 0; i < rowSize; i++)
    {
        let rowArr = [];

        for(let j = 0; j < colSize; j++)
        {
            rowArr.push(new GridObject());
        }
        console.log("dupa")
        gridArray.push(rowArr);
    }

    return gridArray;
    
   }

a w komponencie tylko użył:


  const [gridsArray, setGridsArray] = useState(generateGrid());
0

Ooo działa :)

Mogłem zrobic nawet cos w stylu:

class GridObject{

    constructor(color:string){//, rowIndex:number, colIndex:number){
        this.color = color;
        //this.rowIndex = rowIndex;
        //this.colIndex = colIndex;
    }

    divToReturn()
    {
        return (
            <div className="grid">
            </div>
        )
    }
}

export default GridObject;

oraz:

function GameMap() {

    const [gridsArray, setGridsArray] = useState(generateGrid());

    //setGridsArray(generateGrid());



    return(
        <div>
            {
                gridsArray.map(row => (
                    row.map(el => (
                        //<GridObject color={el.color}></GridObject>
                        el.divToReturn()
                    ))
                ))
            }

        
        </div>
    )
}

export default GameMap;

I smiga :-) Dzieki za podpowiedz. Pytanie tylko czy to "zalecane" podejscie czy raczej unikac tworzenia klas i tworzyc same komponenty?

2

Ja jeszcze dodam, że funkcja generateGrid będzie wywoływana przy każdym renderze komponentu.

Żeby tego uniknąć można przekazać funkcję:

const [gridsArray, setGridsArray] = useState(generateGrid);
const [gridsArray, setGridsArray] = useState(() => generateGrid()); 

https://react.dev/reference/react/useState#examples-initializer

0

W sensie jak sie wywola funkcje to sie wywola tylko raz a jak sie przekaże funkcje "useState(generateGrid);" to sie generuje za kazdym razem lol to dosc istotne :-)

1
goku21 napisał(a):

I smiga :-) Dzieki za podpowiedz. Pytanie tylko czy to "zalecane" podejscie czy raczej unikac tworzenia klas i tworzyc same komponenty?

Stan powinien być raczej niemutowalny. Więc tutaj może być problem. Jak będziesz aktualizować tę siatkę?

Powiedzmy, że chcesz zmienić coś na pozycji x=3, y=4.

I co się wtedy dzieje? Jak to planujesz rozwiązać?

Przekazując to jako stan, raczej wypadałoby, żeby siatka działała wg zasad niemutowalności (czyli żeby się w środku nie zmieniała, tylko żeby zmiana generowała nową siatkę).

wielowymiarowe w useState

Dam ci radę. Jeśli chcesz napisać wielowymiarową tablicę, to łatwiej będzie ci ją napisać albo jako 1-wymiarową tablicę (y * width + x jako wzór na pozycję, gdzie cała tablica ma długość x * y i zawiera kolejne rzędy ciurkiem) albo jako obiekt z kluczami typu x + ',' + y np.

{
   "3,4": true,
   "3,5": false,
   //...
}

i to opakować w jakąś klasę czy coś, żeby łatwiej z tego korzystać.

Dzięki temu jest to płaskie i możesz choćby łatwo zrobić kopię, zamiast babrać się w jakieś wielowymiarowe inicjalizowanie.

Natomiast jeśli ma być to mutowalny obiekt, to lepiej użyć useRef.

Problem w tym, że przy mutowalnym podejściu (czyli masz sobie grid, który się w środku może zmieniać) to wtedy jak coś zmienisz, to React o tym nie będzie wiedzieć. Więc wtedy po każdym apdejcie tego grida, musisz też powodować update komponentu.

0

W takim razie bez sensu w tym przypadku korzystac z klas. Zalozmy jednak ze obiekt gridObject jest bardziej skomplikowany.
Ma w sobie jakas logike, moze nawet chce pobierac za pomoca nodejs jakies dane z serwera.
Oczywiscie moge 1 obiekt dzielic na warstwe GridObjectModel oraz GridObjectView ale tutaj nagle mi sie mnożą pliki, ktore dotycza tego samego obiektu.

Chociaz z drugiej strony jezeli w renderze chcialbym uzyc prostego tagu <GridObject/> to czy on powinien miec jeszcze jakas wieksza logike?
Wlasnie jestem w trakcie przesiadania sie z backendu (nodejs) na frontend z reactem na czele i troche tego nie czuje :/

Poczytam o useRef poniewaz jeszcze sie z tym nie zetknąłem. Dzieki za wskazówkę odnosnie wielowymiarowych tablic.
Na useEfect oraz createContext rzuciłem okiem. Co jeszcze oferuje react co jest czesto uzywane a z jakiegos powodu nie przewija sie to w tutkach? :)

1
goku21 napisał(a):

Ma w sobie jakas logike, moze nawet chce pobierac za pomoca nodejs jakies dane z serwera.

Ale czemu obiekt reprezentujący grid ma pobierać dane z serwera? To brzmi jak kiepski design klasy.

Poczytam o useRef poniewaz jeszcze sie z tym nie zetknąłem

To służy do trzymania referencji do mutowalnych obiektów (często używane do elementów DOM, ale to tylko jedno zastosowanie).

W takim razie bez sensu w tym przypadku korzystac z klas.

minimalistycznie podchodząc można coś takiego zrobić:


function Foo() {
    const [grid, setGrid] = useState({});
    const handleClick = () => {
         setGrid({...grid, '3,4': 'red'});
    };
     
    return <div>
        <button onClick={handleClick}>set (3, 4) to red</button>
    </div>
}

i np. Object.entries do przelatywania po elementach siatki. Albo dwie pętle for:

const size = 20;
for (let y = 0; y < height; y++) {
   for (let x = 0; x < width; x++) {
      const k = x + ',' + y;
      elements.push(<div key={k} style={{background: grid[k], width: size, height: size, position: 'absolute', left: x * size, top: y * size}}></div>)
   }
}

return <div>{elements}</div>
0

"Ale czemu obiekt reprezentujący grid ma pobierać dane z serwera? To brzmi jak kiepski design klasy." Nic nie pobiera, po prostu zastanawiałem sie koncepcyjnie gdyby to byl jakis inny element. Koncepcyjnie przypuśćmy że mamy w jakiejs grze samochod. Samochod moze jechac, skrecac odpalac i gasic silnik i moze np. miec przypisanego kierowce itd.

Fajnie zeby samochod byl 1 obiektem klasy, ktory np. rozszerza jakas klase nadrzedna np. SrodekTransportu.
I teraz taka klasa "Samochód" bedzie miala sporo innej logiki, zmiennych opisujacych ten przedmiot itd.
Dlatego koncepcyjnie sie zastanawiam gdzie sa problemy w pracy z reactem poniewaz raczej nie napisze tego jako komponent w sensie

function foo2 extends foo1 implement foo {

//jakis kod

}

Nie da sie tego powyzej napisac. Chyba ze powiedzmy jakas klasa abstrakcyjna rozszerzalaby component

class AbstractFoo extends component (//react component){

constructor(){
super();

}

render(

)

i dalej powiedzmy

class MovableObject extends AbstractFoo {

let color;
let foo;

constructor{}

move()

turnLeftRight()



}

i na koncu

class Car extends MovableObject{

super();

override turnLeftRight()

runEngine()

stopEngine()

}


Zastanawiałem sie nad takim przypadkiem :)

3

Ale czemu klasa reprezentująca samochód pod kątem logiki miałaby być komponentem React?

Niech logika będzie sobie osobno w klasie, a niech React to tylko wyświetla.

No i wyświetlanie zwykle da się zrobić (prawie) takie same dla wszystkich obiektów. Jeśli robisz grę 2D, to pewnie większość obiektów będzie miało np. przypisany jakiś plik graficzny. Czyli każdy obiekt możesz wyświetlać tak samo. Zwykle nie potrzeba robić osobnego komponentu do wyświetlania samochodu i do wyświetlania autobusu. Po prostu inny plik graficzny (ew. inny kształt typu kółko, prostokąt, inny kolor, rozmiar itp. jeśli idziesz w minimalizm)

Owszem, czasem może zajść potrzeba customizacji wyświetlania. Ale znowu - to można łatwo dodać później. Niezależnie od tego, czy stosujesz klasy

class Car {}

czy robisz to na plain objects np.

const car = {x: 3, y: 4};

to dobrze jest jak obiekty mają jakieś pole type w sobie np.

const car = {type: 'car', x: 3, y: 4};
class Car {
   type = 'car';
}

to ułatwia traktowanie każdego obiektu tak samo, jak również wydobywanie w łatwy sposób informacji o jego typie (i np. wyświetlanie inaczej w zależności od typu).

Bez takiego pola type trzeba byłoby się bawić w instanceof, co nie musi być wygodne.

2
goku21 napisał(a):

W sensie jak sie wywola funkcje to sie wywola tylko raz a jak sie przekaże funkcje "useState(generateGrid);" to sie generuje za kazdym razem lol to dosc istotne :-)

Jak przekażesz funkcję useState(generateGrid); to wykona się tylko przy initial renderze.
Jak wywołasz funkcję useState(generateGrid()); to ona będzie wywoływana przy każdym renderze, ale jej wartość będzie odrzucana.

0

"Niech logika będzie sobie osobno w klasie, a niech React to tylko wyświetla." no ok ale useState w klasie nie uzyje a jak bede chcial cos przerenderowac w zaleznosci od logiki ktora jest w klasie? Wysylac po prostu z klasy jakis eventDispatcher czy przesylac jakas metode z komponentu zeby ja wywolac czy moze jeszcze jakos inaczej?

1

Jak masz mało obiektów, to mógłbyś aktualizować wszystkie naraz. Chociaż jak będzie dużo obiektów, to warto się podpinać pod konkretne obiekty.

Wysylac po prostu z klasy jakis eventDispatcher czy przesylac jakas metode z komponentu zeby ja wywolac czy moze jeszcze jakos inaczej?

no na przykład. Możesz użyć EventTarget https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
albo czegokolwiek innego, co ci da metodę subskrypcji.

Możesz zrobić tak, że każda zmiana w klasie musiałaby iść przez np. metodę update (albo jakkolwiek to nazwiesz). Ta metoda np. emitowałaby event update

class GameObject {
   update() {
      // ...
      this.eventTarget.emit('update');
   }
   subscribe(f) {
      this.eventTarget.addEventListener('update', f);
   }
}

Z kolei od strony Reacta mógłbyś używając useEffect zasubskrybować się pod ten event np.

function Sprite({ gameObject }) {
   const [counter, setCounter] = useState(0);

   useEffect(() => {
    gameObject.subscribe(() => {
        setCounter(x => x + 1);
    );
     return () => {//czyszczenie subskrypcji }
   }, [gameObject]);

   return <div>..........</div>
}

1

Kumam, wlasnie nad ta strukturą sie zastanawiałem. Wiszę piwo :-)

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.