Alberto Vilches
Alberto Vilches

Reputation: 333

useSelector constant not updating after dispatch

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

Upvotes: 3

Views: 16067

Answers (2)

Cody Gordon
Cody Gordon

Reputation: 21

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

Upvotes: 2

skyboyer
skyboyer

Reputation: 23705

After dispatching some action updated store will be provided only on next render. This is the same for functional with hooks and classes with connect HOC.

You need to change your code not to expect changes immediately. It's hard for me understand your intention here, you may start with just rendering what it comes and dispatch-and-forget approach with actions. And it should work.

If it does not, make minimal sample(only relevant hooks + how data is rendered) and also describe what you want to get(instead of "how")

Upvotes: 3

Related Questions