React, why state doesn't change?

Viewed 35

It's a simple example about derived state.

Here is the sandbox url: https://codesandbox.io/s/review-react-derived-state-2-forked-v9ucpr?file=/src/App.js

I expected value change 0 to 10, when I click the button. But, it doesn't rendering also, its flow is weired. I click button several times, it doesn't re-render, even though changing the value.

Here is the code:

import { useState, useEffect } from "react";

export default function App() {
  console.log("App is called!");
  const [value, setValue] = useState(() => {
    console.log("App useState is alloc");
    return 0;
  });
  console.log("App is END");
  return (
    <div>
      <NumberInput value={value} onChange={setValue} />
      <button onClick={() => setValue(10)}>change to ten</button>
    </div>
  );
}

function NumberInput({ value, onChange }) {
  console.log("   NumberInput is called!");
  const [inputValue, setInputValue] = useState(value);

  useEffect(() => {
    const numberValue = Number(inputValue);
    onChange(numberValue);
  }, [inputValue, onChange]);
  console.log("   NumberInput is END");
  return (
    <input value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
  );
}
3 Answers

Your

const [inputValue, setInputValue] = useState(value);

"forks" the prop value to local state, so changes to value won't be reflected in inputValue after initial mount. You'd need

useEffect(() => setInputValue(value), [value]);

in the child component to mirror any changes to the value prop to inputValue too.

You are not updating the internal state of the NumberInput when the value changes.

This would be the code that solves the issue:

import { useState, useEffect } from "react";

export default function App() {
  const [value, setValue] = useState(0);

  return (
    <div>
      <NumberInput value={value} onChange={setValue} />
      <button onClick={() => setValue(10)}>change to ten</button>
    </div>
  );
}

function NumberInput({ value, onChange }) {
  const [inputValue, setInputValue] = useState(value);

  useEffect(() => {
    onChange(+inputValue);
  }, [inputValue, onChange]);

  // We add a use effect that will get trigger when the parent
  // value changes
  // However, this implies that the NumberInput will always render
  // because of the previous useEffect, which also updates the 
  // value's value
  useEffect(() => {
    setInputValue(`${value}`);
  }, [value]);

  return (
    <input value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
  );
}

useState will catch your value only on initialising the child component. Try:

<input value={value}

// instead of 

<input value={inputValue}

I hope that is what you wanted!

Related