简体   繁体   中英

How do I pass a React Context's state through an onClick function created in a method inside that context class?

I have some code that makes an array of div elements to place inside a CSS grid, which is rendered via React since that's extremely fast after the first render. I want to be able to interact with those elements via multiple components, so I set up some context that stores the state of the grid. I'm running into a problem now where I want to set an onClick function on those grid elements so that when they are clicked on, that updates the context's state. In this case I'm trying to keep track of the selected elements by an array. However, I can't seem to reference selectedTiles in the onClick function. It either won't compile, crashes on runtime, or doesn't do anything.

To be clear, if I remove the code lines relating to selectedTiles, it works and toggles the class just fine. I am also binding the function already in the constructor.

Here's the code I have right now, in the three relevant files.

index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import * as serviceWorker from './serviceWorker';
import UserInterface from './components/userinterface';
import DisplayGrid from './components/displaygrid';
import {GridContextProvider} from './gridcontext';

ReactDOM.render(
  <React.StrictMode>
    <GridContextProvider>
      <UserInterface />
      <div className="grid-wrapper">
        <DisplayGrid />
      </div>
    </GridContextProvider>
  </React.StrictMode>,
  document.getElementById('root')
);

serviceWorker.unregister();

displaygrid.js

import React from 'react';
import {GridContext} from '../gridcontext'

class DisplayGrid extends React.Component{
    static contextType = GridContext;
    render(){
        const gridData = this.context;
        return (
            <div>
                {gridData.name}<br />
                <div className= "grid-container" style={{gridTemplateColumns: gridData.columns}}>
                    {gridData.tiles}
                </div>
            </div>
        );
    }
}
export default DisplayGrid;

gridcontext.js

import React from "react";

const GridContext = React.createContext();

class GridContextProvider extends React.Component {
    constructor(props) {
        super(props);
        const topGridData = JSON.parse(localStorage.getItem('grids'))[0];
        this.state = this.makeGrid(topGridData.dims[0], topGridData.dims[1], topGridData.dims[2],
            topGridData.visible, topGridData.hex);
        this.selectTile = this.selectTile.bind(this);
        this.makeGrid = this.makeGrid.bind(this);
    }
    selectTile(event) {
        let tilesArray = this.state.selectedTiles;
        if(event.currentTarget.className === "grid-tile"){
            event.currentTarget.className = "grid-tileb";
            tilesArray.push(event.currentTarget.key);
        } else {
            event.currentTarget.className = "grid-tile";
            tilesArray.splice(tilesArray.indexOf(event.currentTarget.key),tilesArray.indexOf(event.currentTarget.key));
        }
    }
    makeGrid(x, y, tilesize, visible, hex){
        let columnStr = "";
        let tileArray = [];
        const widthStr = tilesize.toString() + "px"
        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 
                        key={"x" + j.toString() + "y" + i.toString()}//for example at coordinates 5,6 id is x5y6.  starts at 0.
                        className="grid-tile"
                        style={{
                            width: widthStr,
                            height: widthStr,
                            border: "1px solid rgba(0, 0, 0," + (visible ? "0.6)" : "0.0)")
                        }}
                        onClick={this.selectTile}
                    >
                    </div>
                )
                tileArray.push(div);
            }
        }
        const gridsDataArray = JSON.parse(localStorage.getItem('grids'));
        const index = JSON.parse(localStorage.getItem('currentGrid'));
        return {
            columns: columnStr,
            tiles: tileArray,
            selectedTiles: [],
            name: gridsDataArray[index].name,
            bgurl: gridsDataArray[index].bgurl
        };
    }
    render() {
        return (
            <GridContext.Provider value={{
                columns: this.state.columns,
                tiles: this.state.tiles,
                selectedTiles: this.state.selectedTiles,
                name: this.state.name,
                bgurl: this.state.bgurl,
                setNameBgurl: (newName, newBgurl) => {
                    this.setState({name : newName,
                    bgurl : newBgurl});
                },
                setGrid: (x, y, tilesize, visible, hex) => {
                    this.setState(this.makeGrid(x, y, tilesize, visible, hex));
                }
            }}>
                {this.props.children}
            </GridContext.Provider>
        );
    }
}

export { GridContext, GridContextProvider };

This code gives me a "TypeError: this is undefined" crash on runtime when I click on a tile, pointing at line 15 of gridcontext, which is let tilesArray = this.state.selectedTiles; which is strange since the method is bound.

Other feedback would be welcome since I'm new to React and not much better with javascript overall; I don't think I've quite wrapped my head around how javascript uses "this". I've been mostly trying to look up tutorials but it seems there are a lot of different ways to use React so it's hard to tell what practices are standard or not.

selectTile(key, className) {
        let tilesArray = this.state.selectedTiles;
        if(className && className === "grid-tile"){
            // do something 
            // if u want to change element's className. maybe u need ref
        } else {
            // do something
        }
}
    makeGrid(x, y, tilesize, visible, hex){
        ...
        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.
                // u may assume 
                const key = "x" + j.toString() + "y" + i.toString()    
                // const className = "grid-tile"
                // just pass whatever u need. that depends on u
                let div = (
                    <div 
                        key={key}//for example at coordinates 5,6 id is x5y6.  starts at 0.
                        className="grid-tile"
                        style={{
                            width: widthStr,
                            height: widthStr,
                            border: "1px solid rgba(0, 0, 0," + (visible ? "0.6)" : "0.0)")
                        }}
                        // the `key` and the `className` depends on u
                        // 
                        onClick={()=> this.selectTile(key, className)}
                    >
                    </div>
                )
                tileArray.push(div);
            }
        }
}

...

I figured out why the error is happening. I use makeGrid in the constructor before it's bound, which references selectTile, and the variable pointer for "this" is somehow set to a stale variable pointer that is undefined. Moving the binds up to the top fixes that error.

Immediately after fixing that error, a second one is found. TypeError: tilesArray is undefined This is because I'm referencing selectTile before the state is fully formed, and it again leaves a stale variable pointer behind.

The constructor must now look like this:

    constructor(props) {
        super(props);
        const topGridData = JSON.parse(localStorage.getItem('grids'))[0];
        this.selectTile = this.selectTile.bind(this);
        this.makeGrid = this.makeGrid.bind(this);
        this.state = {selectedTiles: []}
        this.state = {
            selectedTiles: this.state.selectedTiles,
            ...this.makeGrid(topGridData.dims[0], topGridData.dims[1], topGridData.dims[2],
            topGridData.visible, topGridData.hex)
        }
    }

I immediately ran into a third problem, which is that the initial empty array is being kept as a stale variable pointer despite the attempt to reuse the variable. I changed the onClick to onClick={(event) => this.selectTile(this.state.selectedTiles, event)} to send in the state as an argument, and it works(though I do have to change selectTile to use a temporary array since I can't change the state directly via array mutation). I tried to use onClick={this.selectTile.bind(this, this.state.selectedTiles)} and it turns out that has the same problem as onClick={this.selectTile} .

Also it turns out that "key" is not a property stored in the div element, React kindof yanks it out and stores it internally for dealing with lists. I wound up setting the id property for each element to the same string and refer to that in selectTile for modifying the array.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM