Home > Software design >  Can't log new state right after setting the state
Can't log new state right after setting the state

Time:12-23

I'm trying to get the newest state right after setting it. Note: The change is triggered by typing in the input.

import { useReducer } from "react";

const App = () => {
  const initialState = { value: "Initial state", elements: [] };

  const [state, setState] = useReducer(
    (state: any, updates: any) => ({ ...state, ...updates }),
    initialState
  );

  function handleInputChange(event: any) {
    // setState({
    //   value: event.target.value,
    //   elements: [{ text: event.target.value }]
    // });

    const newState = {
      value: event.target.value,
      elements: [{ text: event.target.value }]
    };

    setState((prevState: any) => {
      return { ...prevState, ...newState };
    });

    console.log(state.value);
    console.log(state.elements);
  }

  return (
    <div>
      <input
        id="input"
        type="text"
        onChange={handleInputChange}
        value={state.value}
      />
    </div>
  );
};

export default App;

With this:

setState({
  value: event.target.value,
  elements: [{ text: event.target.value }]
});

The console.log's would log the old values. For example, if you typed a, they would log: Initial state and [] respectively. Instead of what I expected: a and [Object].

So I tried to set state with a function instead, hoping that I'd get the latest value of state:

setState((prevState: any) => {
  return { ...prevState, ...newState };
});

However, I'm getting Initial state and [] again ... this time not matter how many times I type in the input.

How can I get the latest value of state?

Live code:

Edit useState same value rerender (forked)

CodePudding user response:

The first approach

setState({
  value: event.target.value,
  elements: [{ text: event.target.value }]
});
console.log(state.value);
console.log(state.elements);

results in out-of-date values being logged because the call to a state setter doesn't result in the state being updated immediately. You must wait for the component to re-render before the new values are returned by useReducer and subsequently put into the state variable.

The second approach

setState((prevState: any) => {
  return { ...prevState, ...newState };
});

doesn't work because you're now passing a function to the reducer, so in the reducer:

(state: any, updates: any) => ({ ...state, ...updates }),

the above resolves to

(state: any, updates: any) => ({ ...state, ...theFunctionYouPassed }),

But functions don't have enumerable own properties, so the returned object is no different from the initial state.

useReducer is not the same as useState, and your use of the setState variable name (rather than the more conventional dispatch) may be throwing you off. If you want to use the functional form of state updates (though it wouldn't help anything in this particular situation), use useState instead.

const { useState, useEffect } = React;
const App = () => {
  const [state, setState] = useState({ value: "Initial state", elements: [] });

  function handleInputChange(event) {
    const newState = {
      value: event.target.value,
      elements: [{ text: event.target.value }]
    };
    setState((prevState) => {
      return { ...prevState, ...newState };
    });
  }
  useEffect(() => {
    console.log(state.value);
    console.log(state.elements);
  }, [state]);

  return (
    <div>
      <input
        id="input"
        type="text"
        onChange={handleInputChange}
        value={state.value}
      />
    </div>
  );
};

ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div class='react'></div>

Or change your reducer to accept a function.

const { useReducer, useEffect } = React;

// An example of how you would type and use useReducer
// to accept a callback, like useState's does:

/*
type State = { value: string, elements: Array<{ text: string }>};
const reducer = (
  state: State,
  stateUpdater: (oldState: State) => State
) => stateUpdater(state)
*/
const reducer = (state, stateUpdater) => stateUpdater(state);
const App = () => {
  const [state, dispatch] = useReducer(reducer, { value: "Initial state", elements: [] });

  function handleInputChange(event) {
    const newState = {
      value: event.target.value,
      elements: [{ text: event.target.value }]
    };
    dispatch((prevState) => {
      return { ...prevState, ...newState };
    });
  }
  useEffect(() => {
    console.log(state.value);
    console.log(state.elements);
  }, [state]);

  return (
    <div>
      <input
        id="input"
        type="text"
        onChange={handleInputChange}
        value={state.value}
      />
    </div>
  );
};

ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div class='react'></div>

You also might consider avoiding any, which defeats the purpose of TypeScript - typing things properly helps turn runtime errors into easier-to-solve compile-time errors.

  • Related