简体   繁体   English

React UseContext 更改不重新渲染组件

[英]React UseContext change not re-rendering component

I am trying to make a simple 'Nonogram'/'Picross' game using React to learn UseContext and UseReducer, but am puzzled as to why my top component (App) is not re-rendering when a value it uses changes.我正在尝试使用 React 制作一个简单的“Nonogram”/“Picross”游戏来学习 UseContext 和 UseReducer,但我对为什么我的顶级组件(应用程序)在它使用的值发生变化时没有重新渲染感到困惑。 Perhaps I am missing something basic, but I've read through documentation and examples online and can't see why it is not re-rendering.也许我遗漏了一些基本的东西,但我已经在线阅读了文档和示例,但看不出它为什么不重新渲染。

Expectation: User goes on the application, clicks on the squares to change their value (draw a cross by clicking on the squares), and the text underneath the board reads "Congratulations,", as it is based on the value of 'isComplete'预期:用户打开应用程序,单击方块以更改它们的值(通过单击方块画一个十字),并且板下方的文本显示“Congratulations”,因为它基于“isComplete”的值

Problem: As above, but 'Keep trying' remains.问题:如上所述,但“继续尝试”仍然存在。

I added a button to see the boardState as defined in the UseReducer function, too.我添加了一个按钮来查看在 UseReducer function 中定义的 boardState。

Code is as follows:代码如下:

App.js应用程序.js

import './App.css';
import { useReducer } from 'react';
import Table from './Table';
import BoardContext from './BoardContext';
import boardReducer from './BoardReducer';

function App() {
  //Puzzle layout
  const puzzleArray = [
    [true, false, true], 
    [false, true, false], 
    [true, false, true]
  ];

  //Creating a set of blank arrays to start the game as the userSelection
  const generateUserSelection = () => {
    const userSelection = [];
    puzzleArray.forEach(row => {
      let blankRow = [];
      row.forEach(square => {
        blankRow.push(false)
      });
      userSelection.push(blankRow);
    })
    return userSelection;
  };

  //Initial Context value
  const boardInfo = {
    puzzleName: "My Puzzle",
    puzzleArray: puzzleArray,
    userSelection: generateUserSelection(),
    isComplete: false
  };

  const [ boardState, dispatch ] = useReducer(boardReducer, boardInfo)

  return (
    <BoardContext.Provider value={{board: boardState, dispatch}}>
      <div className="App">
        <header className="App-header">
          <p>
            Picross
          </p>
          <Table />
        </header>
        <div>
          {boardState.isComplete ? 
            <div>Congratulations!</div>
          : <div>Keep trying</div>
          }
        </div>
        <button onClick={() => console.log(boardState)}>boardState</button>
      </div>
    </BoardContext.Provider>
  );
}

export default App;

Table.jsx:表.jsx:

import { useContext, useEffect } from 'react';
import './App.css';
import Square from './Square';
import BoardContext from './BoardContext';

function Table() {

    useEffect(() => {console.log('table useEffect')})

    const { board } = useContext(BoardContext);

    const generateTable = solution => {
        const squareLayout = []

        for (let i = 0; i < solution.length; i++) {
            const squares = []
            for (let j = 0; j < solution[i].length; j++) {
                squares.push(
                    <Square 
                        position={{row: i, column: j}}
                    />
                );
            };
            squareLayout.push(
                <div className="table-row">
                    {squares}
                </div>
            );
        };
        return squareLayout;
    };

  return (
    <div className="grid-container">
        {generateTable(board.puzzleArray)}
    </div>
  );
}

export default Table;

Square.jsx Square.jsx

import { useContext, useState } from 'react';
import './App.css';
import BoardContext from './BoardContext';

function Square(props) {
  
    const { board, dispatch } = useContext(BoardContext)

    const [ isSelected, setIsSelected ] = useState(false);
    const { position } = props;

    const handleToggle = () => {
        console.log(board)
        board.userSelection[position.row][position.column] = !board.userSelection[position.row][position.column]
        dispatch(board);
        setIsSelected(!isSelected);
    }

    return (
    <div className={`square ${isSelected ? " selected" : ""}`}
        onClick={handleToggle}
    >
        {position.row}, {position.column}
    </div>
  );
}

export default Square;

Thanks谢谢

Edit: I know for a simple application like this it would be very easy to pass down state through props, but the idea is to practice other hooks, so wanting to avoid it.编辑:我知道对于像这样的简单应用程序,通过道具传递 state 非常容易,但想法是练习其他钩子,所以想避免它。 The ideas I am practicing in this would ideally be extensible to bigger projects in the future.我在这方面实践的想法理想情况下可以扩展到未来更大的项目。

Edit 2: As requested, here's my BoardReducer.js file:编辑 2:根据要求,这是我的 BoardReducer.js 文件:

const boardReducer = (state, updateInfo) => {

    let isComplete = false;

    if (JSON.stringify(updateInfo.userSelection) === JSON.stringify(state.puzzleArray)) {
        isComplete = true;
    }

    updateInfo.isComplete = isComplete;
    return updateInfo;
}

export default boardReducer;

(using JSON.stringify as a cheap way to check matching arrays as it's only a small one for now!) (使用 JSON.stringify 作为检查匹配 arrays 的廉价方法,因为它现在只是一个小方法!)

Issue问题

You are mutating your state object in a couple places:你在几个地方改变你的 state object :

const handleToggle = () => {
  console.log(board);
  board.userSelection[position.row][position.column] = !board.userSelection[position.row][position.column]; // <-- mutation!
  dispatch(board);
  setIsSelected(!isSelected);
}

And in reducer在减速机中

const boardReducer = (state, updateInfo) => {
  let isComplete = false;

  if (JSON.stringify(updateInfo.userSelection) === JSON.stringify(state.puzzleArray)) {
    isComplete = true;
  }

  updateInfo.isComplete = isComplete; // <-- mutation!
  return updateInfo; // <-- returning mutated state object
}

Since no new state object is created React doesn't see a state change and doesn't rerender your UI.由于没有创建新的 state object,因此 React 没有看到 state 更改并且不会重新呈现您的 UI。

Solution解决方案

useReducer will typically employ a "redux" pattern where the reducer function consumes the current state and an action to operate on that state, and returns a new state object. useReducer will typically employ a "redux" pattern where the reducer function consumes the current state and an action to operate on that state, and returns a new state object.

You should dispatch an action that toggles the user selection and checks for a complete board.您应该发送一个操作来切换用户选择并检查是否有完整的板。

Board Reducer板式减速机

When updating state you should shallow copy any state objects that you are updating into new object references, starting with the entire state object. When updating state you should shallow copy any state objects that you are updating into new object references, starting with the entire state object.

const boardReducer = (state, action) => {
  if (action.type === "TOGGLE") {
    const { position } = action;
    const nextState = {
      ...state,
      userSelection: state.userSelection.map((rowEl, row) =>
        row === position.row
          ? rowEl.map((colEl, col) =>
              col === position.column ? !colEl : colEl
            )
          : rowEl
      )
    };

    nextState.isComplete =
      JSON.stringify(nextState.userSelection) ===
      JSON.stringify(state.puzzleArray);

    return nextState;
  }
  return state;
};

Create an action creator, which is really just a function that returns an action object.创建一个动作创建器,它实际上只是一个返回动作 object 的 function。

const togglePosition = position => ({
  type: "TOGGLE",
  position
});

Then the handleToggle should consume/pass the row and column position in a dispatched action.然后, handleToggle应该在调度的操作中消耗/传递行和列 position。

const handleToggle = () => dispatch(togglePosition(position));

Simple Demo简单演示

编辑 react-usecontext-change-not-re-rendering-component

在此处输入图像描述

Demo Code:演示代码:

const puzzleArray = [
  [true, false, true],
  [false, true, false],
  [true, false, true]
];

const userSelection = Array(3).fill(Array(3).fill(false));

const togglePosition = (row, column) => ({
  type: "TOGGLE",
  position: { row, column }
});

const boardReducer = (state, action) => {
  if (action.type === "TOGGLE") {
    const { position } = action;
    const nextState = {
      ...state,
      userSelection: state.userSelection.map((rowEl, row) =>
        row === position.row
          ? rowEl.map((colEl, col) =>
              col === position.column ? !colEl : colEl
            )
          : rowEl
      )
    };

    nextState.isComplete =
      JSON.stringify(nextState.userSelection) ===
      JSON.stringify(state.puzzleArray);

    return nextState;
  }
  return state;
};

export default function App() {
  const [boardState, dispatch] = React.useReducer(boardReducer, {
    puzzleArray,
    userSelection,
    isComplete: false
  });

  const handleClick = (row, column) => () =>
    dispatch(togglePosition(row, column));

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>

      <div>{boardState.isComplete ? "Congratulations!" : "Keep Trying"}</div>

      <div>
        {boardState.userSelection.map((row, r) => (
          <div key={r}>
            {row.map((col, c) => (
              <span
                key={c}
                className={classnames("square", { active: col })}
                onClick={handleClick(r, c)}
              />
            ))}
          </div>
        ))}
      </div>
    </div>
  );
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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