React UseContext 更改不重新渲染组件

时间:2021-01-31 20:52:22

标签: javascript reactjs react-hooks use-context use-reducer

我正在尝试使用 React 制作一个简单的“Nonogram”/“Picross”游戏来学习 UseContext 和 UseReducer,但是我很困惑为什么我的顶级组件(应用程序)在它使用的值发生变化时没有重新渲染。也许我遗漏了一些基本的东西,但我已经阅读了在线文档和示例,但不明白为什么不重新渲染。

预期:用户打开应用程序,点击方块来改变它们的值(点击方块画一个十字),并且板子下面的文字写着“恭喜!”,因为它是基于值的'已完成'

问题:如上所述,但“继续尝试”仍然存在。

我也添加了一个按钮来查看在 UseReducer 函数中定义的 boardState。

代码如下:

App.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:

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

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;

谢谢

编辑:我知道对于像这样的简单应用程序,通过 props 传递状态会很容易,但这个想法是练习其他钩子,所以想避免它。理想情况下,我正在实践的想法将来可以扩展到更大的项目。

编辑 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;

(使用 JSON.stringify 作为检查匹配数组的一种廉价方法,因为它现在只是一个小方法!)

1 个答案:

答案 0 :(得分:1)

问题

你在几个地方改变你的状态对象:

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

在减速器中

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
}

由于没有创建新的状态对象,React 不会看到状态更改,也不会重新渲染您的 UI。

解决方案

useReducer 通常会采用“redux”模式,其中 reducer 函数消耗当前的 state 和一个 action 来对该状态进行操作,并返回一个 新的状态对象。

您应该发送一个操作来切换用户选择并检查完整的板。

减速板

更新状态时,您应该将要更新的任何状态对象浅复制到新的对象引用中,从整个 state 对象开始。

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;
};

创建一个动作创建器,它实际上只是一个返回动作对象的函数。

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

然后 handleToggle 应该在分派的操作中消耗/传递行和列位置。

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

简单演示

Edit react-usecontext-change-not-re-rendering-component

enter image description here

演示代码:

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>
  );
}