React: Update Child from Parent (Parent being a matrix and child being a cell)

Viewed 27

I am trying to build a maze generator using react that displays every step of the generation (every wall opening). So far I have a grid component (parent) and a cell component (child)

The grid component generates a m * n matrix filled with cell components. A cell has four walls that can be removed or added (css border- property).

I am at the part where I want to trigger the maze generation (by clicking a button). I think it should sit in the grid component since it has the grid of cells, but I can't figure out how to change the cell's state from the grid. I get that in some scenarios you want to keep the child's state in the parent but keeping a state for every single wall of every single cell within the grid seems weird.

The algorithm picks a random cells and opens the walls from the current cell to the chosen cell. Can I achieve such a behavior from the grid or do I need to put my mazeGeneration function somewhere else?

Thank you

Here's my current code:

GridComponent

const Grid = (props) => {

    const generateGrid = () => {
        let grid = [];

        for (let i = 0; i < nbRows; i++) {
            let curRow = [];
            for (let j = 0; j < nbCols; j++) {
                curRow.push(
                    <Cell
                        key={`[${i}][${j}]`}
                        posX={i}
                        posY={j}
                        cellSizeInPx={cellSizeInPx}
                        className={'cell'}
                    />)
            }
            grid.push(curRow);
        }

        return grid;
    }


    const { nbCols, nbRows, cellSizeInPx } = props;
    const [grid, setGrid] = useState(generateGrid);

    return (
        <table className='grid' >
            <tbody>
                {
                    grid.map((row, idx) => {
                        return <tr key={idx}>{row}</tr>
                    })
                }

            </tbody>
        </table>
    )
}

CellComponent

const Cell = (props) => {
    const [hasTopWall, setTopWall] = useState(true);
    const [hasRightWall, setRightWall] = useState(true);
    const [hasBottomWall, setBottomWall] = useState(true);
    const [hasLeftWall, setLeftWall] = useState(true);


    const { className, posX, posY } = props

    return (
        <td
            className={`cell 
            ${hasTopWall ? "top" : ""}
            ${hasRightWall ? "right" : ""}
            ${hasBottomWall ? "bottom" : ""}
            ${hasLeftWall ? "left" : ""}  `}
        >

        </td>
    )

}
export default Cell;

Cell.css

.cell {
    width: 20px;
    height: 20px;
}

td {
    padding: 0;
}

.top {
    border-top: 1px solid rgb(175, 216, 248);
}

.right {

    border-right: 1px solid rgb(175, 216, 248);
}

.bottom {
    border-bottom: 1px solid rgb(175, 216, 248);

}

.left {
    border-left: 1px solid rgb(175, 216, 248);
}
1 Answers

I get that in some scenarios you want to keep the child's state in the parent but keeping a state for every single wall of every single cell within the grid seems weird.

Not necessarily - if the grid needs to change the state of the cells, then having the state in the grid makes sense. Or, you could use context to better separate state and behaviour. Something like:

const MazeContext = createContext();

const MazeContextProvider = ({ children }) => {
  const [cells, setCells] = useState([]);

  const handleGenerateGrid = () => setCells(generateGrid(10, 10));
  
  const context = {
    cells,
    onGenerateGrid: handleGenerateGrid,
  };

  return (
    <MazeContext.Provider value={context}>
      {children}
    </MazeContext.Provider>
  );
}

// Some function to generate your initial grid - keep this OUT of the context,
// there's no need to recreate the function on every render.
function generateGrid(rowCount, colCount) {
  const grid = [];

  for (let col = 0; col < colCount; col++) {
    const rowCells = [];

    for (let row = 0; row < rowCount; row++) {
      rowCells.push({
        posX: row,
        posY: col,
        // some other state for your cell
      });
    }

    grid.push(rowCells);
  }

  return grid;
}

// Helper hooks
const useMazeContext = () => useContext(MazeContext);

const useCell = (posX, posY) => {
  const { cells } = useMazeContext();
  return cells[posX][posY];
};

Then you just have to wrap anything concerned with your maze inside the context provider -

export default function App() {
  return (
    <MazeContextProvider>
      <Grid />
    </MazeContextProvider>
  );
}

And now your grid can interact with the context -

const Grid = () => {
  const { cells, onGenerateGrid } = useMazeContext();

  return (
    <>
      <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
        {cells.map((colCells, colIndex) => (
          <div key={colIndex} style={{ display: 'flex', gap: '8px' }}>
            {colCells.map((cell, rowIndex) => (
              <div key={rowIndex}>Cell {cell.posX},{cell.posY}</div>
            ))}
          </div>
        ))}
      </div>

      <input type="button" value="Generate Grid" onClick={onGenerateGrid} />
    </>
  );
}

Here's a simple demo.

Related