Home > database >  Setting state to a zero-filled array with Immer in react-toolkit
Setting state to a zero-filled array with Immer in react-toolkit

Time:02-09

I was following the TS usage docs and now I'm trying to clear an array using Redux Toolkit but my turnOffAll(state) reducer causes thousands of rerenders (console log outputs) and an error about it in the browser.

I have read that it's possible to return state, rather than "mutate" it from within turnOffAll. So I've tried a variation of turnOffAll having: return Array(screenSize.width * screenSize.height).fill(0) but this causes an error in app.jsx that dispatch(turnOffAll()); does not provide enough arguments.

Another approach I tried in turnOffAll is state = initialState; but while this doesn't cause unnececary rerenders, the state is not reset if I do a console.log(useAppSelector((state) => state.pixels)); in app.jsx.

The relevant info from the files are below. This code is the situation in my first paragraph above and causes thousands of rerenders when calling dispatch(turnOffAll()); from app.jsx:

pixelsSlice.ts:

import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import type { RootState } from "./store";
import { Pixel, ScreenSize } from "./types";

//Defining a type for the slice state
interface PixelsState {
  pixels: Array<number>;
}

const screenSize = { width: 128, height: 59 };

//Define the initial state using the above type.
const initialState: PixelsState = {
  pixels: Array(screenSize.width * screenSize.height).fill(0),
};

export const pixelsSlice = createSlice({
  name: "pixels",
  initialState,
  reducers: {
    turnOn(state, action: PayloadAction<Pixel>) {
      //This may look like mutating state, but Immer makes it ok:
      state.pixels[
        screenSize.width * action.payload.row   action.payload.column
      ] = 1;
    },
    turnOffAll(state) {
      state = initialState;
    },
  },
});

export const { turnOn, turnOffAll } = pixelsSlice.actions;

//Other code such as selectors can use the imported `RootState` type...
export const selectPixels = (state: RootState) => state.pixels;

export default pixelsSlice.reducer;

app.tsx:

import React, { useEffect } from "react";
import { useAppSelector, useAppDispatch } from "./app/hooks";
import { turnOn, turnOffAll } from "./app/pixelsSlice";
import type { Pixel } from "./app/types";

function App() {
  const dispatch = useAppDispatch();

  //Demonstrating reading and writing pixels from the redux store.
  console.log(useAppSelector((state) => state.pixels));

  let p: Pixel = { row: 1, column: 2 };
  dispatch(turnOn(p));
  console.log(useAppSelector((state) => state.pixels));

  dispatch(turnOffAll()); //THIS CAUSES THOUSANDS OF RERENDERS AND CONSOLE LOG OUTPUTS :(
  console.log(useAppSelector((state) => state.pixels));

  return <div className="App"></div>;
}

export default App;

hooks.ts:

import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "./store";

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

CodePudding user response:

You are breaking a very fundamental rule of React - not even Redux - here:

You trigger side effects during render.
React can trigger that render function any amount of times and not commit it to the DOM. It can be triggered because a parent component rerenders, because state changes, any number of reasons. One such reason might be that your Redux state changes.

So you dispatch, change state, trigger a rerender, dispatch again, which changes state (setting to another empty array - [] !== []) and triggers yet another render.

It is of utmost importance that your side effects like dispatches or state setter calls always happen in a useEffect or an event handler - and in the case of the useEffect that you also use a correct dependency array - hit the React documentation on that.

Also, your workflow of dispatch - useSelector - dispatch useSelector will probably not do what you want here. State reliably only changes between renders, not within a render.

CodePudding user response:

In addition to @phry 's comment, the line state = in an Immer-powered reducer is always incorrect. You need to either mutate a value inside of state, or return a new value.

If you want to replace the existing state, do return initialState.

See the RTK usage guide page on "Writing Reducers with Immer" for more details.

  •  Tags:  
  • Related