Home > front end >  useReducer - keydown handler re-evaluates initial state on each render
useReducer - keydown handler re-evaluates initial state on each render

Time:02-27

https://codesandbox.io/s/suspicious-chebyshev-vy4mf2

I have a useReducer hook and useEffect with a keydown event handler. The issue is that on each re-render resulting from handling the keyboard button press, the component function re-runs the function obtaining initial state.

The visible incorrect result is the random number logged in console on each render (I expect no logging). In other words, the goal here is to stop getInitialState function running on every render.

import { useEffect, useReducer } from "react";
import "./styles.css";

interface IAction {
  type: string;
  payload: string;
}

export default function App() {
  const reducer = (state: string, action: IAction) => {
    switch (action.type) {
      case "KEYDOWN":
        return action.payload;
      default:
        return state;
    }
  };

  const getInitialState = () => {
    const rand = Math.random().toString();
    console.log(rand);
    return rand;
  };

  const [state, dispatch] = useReducer(reducer, getInitialState());

  const keyDownHandler = (e: KeyboardEvent): void => {
    dispatch({
      type: "KEYDOWN",
      payload: e.key
    });
  };

  useEffect(() => {
    document.addEventListener("keydown", keyDownHandler);
    return () => document.removeEventListener("keydown", keyDownHandler);
  }, [state, dispatch]);

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

CodePudding user response:

When a component sees that it needs to re-render, its function will run again. What you're doing is equivalent to asking why

export default function App() {
  // ...
  const [state, dispatch] = useReducer(reducer, getInitialState());

runs getInitialState every time App runs, which is equivalent to

export default function App() {
  // ...
  const parameterToPass = getInitialState();
  const [state, dispatch] = useReducer(reducer, parameterToPass);

Which should make what's going on clear.

It's not that useReducer needs to calculate the initial value again - it doesn't, it only uses that second parameter the first time it runs, to determine the initial state. It's that you're calling the function that calculates the initial state every time the App re-renders.

As long as you make sure the function that calculates the initial state doesn't have side-effects, feel free to just ignore it - what you're doing now is just fine.

Another option is to pass a third argument to useReducer as the function to invoke to calculate the initial state.

const [state, dispatch] = useReducer(reducer, null, getInitialState);

CodePudding user response:

You were calling the getInitialState on every render, that doesn't mean it was getting re-initialized.

You can avoid this by running the initializer in the 3rd argument position which is designed for function usage.

You should also create the function for the reducer outside of the render function, otherwise it re-runs more often than it should.

const { useEffect, useReducer } = React;

interface IAction {
  type: string;
  payload: string;
}
const reducer = (state: string, action: IAction) => {
  switch (action.type) {
    case "KEYDOWN":
      return action.payload;
    default:
      return state;
  }
};
function App() {
  const getInitialState = () => {
    const rand = Math.random().toString();
    console.log(rand);
    return rand;
  };

  const [state, dispatch] = useReducer(reducer, null, getInitialState);

  const keyDownHandler = (e: KeyboardEvent): void => {
    dispatch({
      type: "KEYDOWN",
      payload: e.key
    });
  };

  useEffect(() => {
    document.addEventListener("keydown", keyDownHandler);
    return () => document.removeEventListener("keydown", keyDownHandler);
  }, [state, dispatch]);

  return <div className="App">{state}</div>;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
    <div id="root"></div>

  • Related