Reputation: 333
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
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
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