useSelector 常量在分派后不更新 [英] useSelector constant not updating after dispatch

查看:26
本文介绍了useSelector 常量在分派后不更新的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

这是一个Codesandbox.

getIDs() 更新 cells,然后 initializeCells() 需要它.但是,此更改不会在分发操作后反映出来.尽管如此,我可以在 Redux 开发工具上看到动作通过并且 cells 的值已相应更改.gameStart() 正在通过 props 传递给 cells,一个子组件,并通过 useEffect() 钩子在那里调用.我需要传递一个空数组作为这个钩子的第二个参数,否则它将永远运行,因为每次调用它时状态都会更新.问题在于,在第一次运行 getIDs() 之后,新状态不适用于以下函数.似乎是 gameStart() 完全完成并再次被调用.我需要在 getIDs() 完成后立即更新 initializeCells() 需要更新的状态.

cells.js

import React, { useEffect } from "react";import { useSelector } from "react-redux";从./Container/Container/Cell"导入单元格;const Cells = props =>{const board = useSelector(state => state.board);useEffect(() => {props.gameStart();}, []);返回 (<div id="单元格">{board.map(cell => {返回 (<单元格id={cell.id.substring(1)}键={cell.id.substring(1)}类名=单元格"/>);})}

);};导出默认单元格;

app.js

import React, { useEffect } from "react";import { useSelector, useDispatch } from "react-redux";进口 {设置单元格,棋盘} 来自../../redux/actions/index";const 游戏 = () =>{const dispatch = useDispatch();const 单元格 = useSelector(state => state.cells);const board = useSelector(state => state.board);const boardSize = useSelector(state => state.boardSize);异步函数 gameStart() {等待 getIDs();控制台日志(单元格);//[]等待 initializeCells();等待assignSnake();等待 placeFood();等待paintCells();}函数 getIDs() {让 cellID = "";让集合 = [];for (让 i = 1; i <= boardSize.rows; i++) {for (让 j = 1; j <= boardSize.columns; j++) {cellID = `#cell-${i}-${j}`;collection.push(cellID);}}调度(setCells(集合));控制台日志(单元格);//[]}函数 initializeCells() {控制台日志(单元格);//[]常量板 = [];//for 循环永远不会运行,因为单元格为空for (let i = 0; i 

reducers/index.js

import {SET_CELLS,SET_BOARD} 来自../constants/action-types";常量初始状态 = {木板: [],细胞: [],板尺寸:{行数:25,列:40}};const rootReducer = (state = initialState, action) =>{开关(动作.类型){案例SET_CELLS:返回 Object.assign({}, state, {单元格:action.payload});案例SET_BOARD:返回 Object.assign({}, state, {板:action.payload});默认:返回状态;}};

actions/index.js

import {SET_CELLS,SET_BOARD} 来自../constants/action-types";导出 const setCells = 有效载荷 =>{返回{类型:SET_CELLS,有效载荷};};export const setBoard = 有效载荷 =>{返回{类型:SET_BOARD,有效载荷};};

常量/action-types.js

export const SET_CELLS = "SET_CELLS";出口 const SET_BOARD = "SET_BOARD";

解决方案

我建议您重新考虑这里的所有模式,并在编写代码之前考虑哪些因素会影响您的决定.首先,为什么要这样设置状态?如果使用状态是合理的,当您仅访问 Cells 中的 board 时,为什么要创建单独的 cellsboard 状态值代码> 组件?是否会控制诸如 boardSize 之类的任何值?也许当应用程序加载时它们会从远程网络资源被调用,而您并不马上知道它们?如果对其中任何一个都没有,那么实际上没有任何充分的理由将它们存储在状态中,并且它们可以只是在组件外部初始化时声明的常量.如果是,代码应该适合用例.如果您打算让用户控制电路板大小,您应该使用默认值初始化您的值,并处理所有同步状态更改,而不会在您的减速器中产生副作用.

另外,正如您所知,您使用异步函数的方式是一种反模式.使用 Redux,如果您使用真正的异步函数,即调用网络资源,您可以使用 thunks,并在每次需要在 dispatch 后访问更新的状态时在 thunk 中调用 getState().

否则,你熟悉componentDidUpdate的类组件生命周期模式吗?本质上,您监听"状态更改,并且仅在更改后调用依赖于更改状态的函数.使用钩子可以做到的一种方法是 useEffect 使用包含您所依赖的状态的依赖项数组,这意味着它只会在这些依赖项发生更改时被调用,并且您可以进行进一步的条件检查在 useEffect 函数中(但从不useEffect 包裹在条件中!).但是,当使用对象或数组作为依赖项时,事情会变得更加复杂,因为它使用严格的相等检查,因此您可能需要使用 ref 并比较 useEffect 中的当前值和先前值,例如 <一个 href="https://usehooks.com/usePrevious/" rel="nofollow noreferrer">这个 usePrevious 钩子.

综上所述,在您当前的用例中,您不需要执行任何这些操作,因为您只是同步初始化静态值.如果 boardSize 值不受控制,我个人甚至不会将其存储在 state 中,但为了教育起见,您将如何在 reducer 中执行此操作.

首先,从 Game 组件中简单地disptach({ type: 'INITIALIZE_BOARD' }).

然后,将所有同步逻辑封装在减速器中:

const initialState = {木板: [],板尺寸:{行数:25,列:40}};const rootReducer = (state = initialState, action) =>{开关(动作.类型){案例'INITIALIZE_BOARD':{const { 行,列} = state.boardSize常量板 = [];for (let row = 1; row <= rows; row++) {for (let column = 1; column <= columns; column++) {板推({id:`#cell-${row}-${column}`,排,柱子,hasFood:假,hasSnake:假});}}返回 {...状态,木板};}默认:返回状态;}};

Here is a Codesandbox.

getIDs() updates cells, which is then needed by initializeCells(). However, this change is not reflected after dispatching the action. Despite that, I can see on Redux dev tools that the action got through and the value of cells has changed accordingly. gameStart() is being passed via props to cells, a child component, and called there via the useEffect() hook. I need to pass an empty array as the second argument for this hook or it will run forever, as the state updates every time it's called. The problem is that the new state is not available for the following functions after getIDs() on its first run. It seems to be when gameStart() has fully finished and has been called again. I need to have the piece of state that initializeCells() needs updated right after getIDs() is done.

cells.js

import React, { useEffect } from "react";
import { useSelector } from "react-redux";

import Cell from "./Container/Container/Cell";

const Cells = props => {
  const board = useSelector(state => state.board);

  useEffect(() => {
    props.gameStart();
  }, []);

  return (
    <div id="cells">
      {board.map(cell => {
        return (
          <Cell
            id={cell.id.substring(1)}
            key={cell.id.substring(1)}
            className="cell"
          />
        );
      })}
    </div>
  );
};

export default Cells;

app.js

import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";

import {
  setCells,
  setBoard
} from "../../redux/actions/index";

const Game = () => {
  const dispatch = useDispatch();

  const cells = useSelector(state => state.cells);
  const board = useSelector(state => state.board);
  const boardSize = useSelector(state => state.boardSize);

  async function gameStart() {
    await getIDs();
    console.log(cells); // []
    await initializeCells();
    await assignSnake();
    await placeFood();
    await paintCells();
  }

  function getIDs() {
    let cellID = "";
    let collection = [];

    for (let i = 1; i <= boardSize.rows; i++) {
      for (let j = 1; j <= boardSize.columns; j++) {
        cellID = `#cell-${i}-${j}`;

        collection.push(cellID);
      }
    }
    dispatch(setCells(collection));
    console.log(cells); // []
  }

  function initializeCells() {
    console.log(cells); // []
    const board = [];
    // for loop never runs because cells is empty
    for (let i = 0; i < cells.length; i++) {
      board.push(cell(cells[i]));
    }
    dispatch(setBoard(board));
    console.log("Board: ", board); // []
  }

  function cell(id) {
    return {
      id: id,
      row: id.match("-(.*)-")[1],
      column: id.substr(id.lastIndexOf("-") + 1),
      hasFood: false,
      hasSnake: false
    };
  }

  return (
  ...
  )
}

export default Game;

reducers/index.js

import {
  SET_CELLS,
  SET_BOARD
} from "../constants/action-types";

const initialState = {
  board: [],
  cells: [],
  boardSize: {
    rows: 25,
    columns: 40
  }
};

const rootReducer = (state = initialState, action) => {
  switch (action.type) {
    case SET_CELLS:
      return Object.assign({}, state, {
        cells: action.payload
      });

    case SET_BOARD:
      return Object.assign({}, state, {
        board: action.payload
      });

    default:
      return state;
  }
};

actions/index.js

import {
  SET_CELLS,
  SET_BOARD
} from "../constants/action-types";

export const setCells = payload => {
  return { type: SET_CELLS, payload };
};

export const setBoard = payload => {
  return { type: SET_BOARD, payload };
};

constants/action-types.js

export const SET_CELLS = "SET_CELLS";
export const SET_BOARD = "SET_BOARD";

解决方案

I suggest you rethink all the patterns here and think about what is informing your decisions before writing code. First, why set the state like this at all? If using state is justified, why create separate cells and board state values when you're only accessing board in your Cells component? Are any of the values such as boardSize going to be controlled? Perhaps they will be called from a remote network resource when the app loads and you don't know them right away? If no to either of those, there's not really any good reason to store them in state, and they can just be constants declared at initialization outside of your components. If yes, the code should fit the use-case. If you are going to have user controlled board size, you should initialize your values with defaults and handle all synchronous state changes with no side effects within your reducer.

Also, just so you know, the way you're using async functions is kind of an anti-pattern. With Redux, if you're using true async functions, i.e. calling a network resources, you could use thunks, and call getState() within a thunk each time you need to access the updated state after a dispatch.

Otherwise, are you familiar with the class component lifecycle pattern of componentDidUpdate? Essentially you "listen" for state changes and only call a function that relies on changed state after it changes. One way you can do that with hooks is useEffect with a dependency array containing the state you're relying on, meaning it'll only be called when those dependencies are changed, and you can do further conditional checks within the useEffect function (but never wrap useEffect in a conditional!). Things get more complicated here when using objects or arrays as dependencies, however, as it uses a strict equality check so you may need to use a ref and compare current and previous values within useEffect, with something like this usePrevious hook.

All that said, in your current use-case you don't need to do any of this because you're simply synchronously initializing static values. I would personally not even store this in state if the boardSize values are not controlled, but for the sake of education here's how you'd do it in the reducer.

First, simply disptach({ type: 'INITIALIZE_BOARD' }) from the Game component.

Then, encapsulate all your synchronous logic within your reducer:

const initialState = {
  board: [],
  boardSize: {
    rows: 25,
    columns: 40
  }
};

const rootReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'INITIALIZE_BOARD': {
      const { rows, columns } = state.boardSize
      const board = [];

      for (let row = 1; row <= rows; row++) {
        for (let column = 1; column <= columns; column++) {
          board.push({
            id: `#cell-${row}-${column}`,
            row,
            column,
            hasFood: false,
            hasSnake: false
          });
        }
      }

      return {
        ...state,
        board
      };
    }
    default:
      return state;
  }
};

这篇关于useSelector 常量在分派后不更新的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆