Notbad
Notbad

Reputation: 6296

input value not updating when mutating state

While creating a little project for learning purposes I have come across an issue with the updating of the input value. This is the component (I have tried to reduce it to a minimum).

function TipSelector({selections, onTipChanged}: {selections: TipSelectorItem[], onTipChanged?:(tipPercent:number)=>void}) {
    
    const [controls, setControls] = useState<any>([]);
    const [tip, setTip] = useState<string>("0");

    function customTipChanged(percent: string)  {
        setTip(percent);
    }

    //Build controls
    function buildControls()
    {
        let controlList: any[] = [];
        controlList.push(<input className={styles.input} value={tip.toString()} onChange={(event)=> {customTipChanged(event.target.value)}}></input>);
        setControls(controlList);
    }
    
    useEffect(()=>{
        console.log("TipSelector: useEffect");
        buildControls();

        return ()=> {
            console.log("unmounts");
        }
    },[])
    
    console.log("TipSelector: Render -> "+tip);
    return (
        <div className={styles.tipSelector}>
            <span className={globalStyles.label}>Select Tip %</span>
            <div className={styles.btnContainer}>
                {
                   controls
                }
            </div>
        </div>
    );
}

If I move the creation of the input directly into the return() statement the value is updated properly.

Upvotes: 1

Views: 114

Answers (2)

Squiggs.
Squiggs.

Reputation: 4474

I'd move your inputs out of that component, and let them manage their own state out of the TipSelector.

See:

https://codesandbox.io/s/naughty-http-d38w9

e.g.:

import { useState, useEffect } from "react";
import CustomInput from "./Input";

function TipSelector({ selections, onTipChanged }) {
  const [controls, setControls] = useState([]);

  //Build controls
  function buildControls() {
    let controlList = [];
    controlList.push(<CustomInput />);
    controlList.push(<CustomInput />);
    setControls(controlList);
  }

  useEffect(() => {
    buildControls();

    return () => {
      console.log("unmounts");
    };
  }, []);

  return (
    <div>
      <span>Select Tip %</span>
      <div>{controls}</div>
    </div>
  );
}

export default TipSelector;

import { useState, useEffect } from "react";

function CustomInput() {
  const [tip, setTip] = useState("0");

  function customTipChanged(percent) {
    setTip(percent);
  }

  return (
    <input
      value={tip.toString()}
      onChange={(event) => {
        customTipChanged(event.target.value);
      }}
    ></input>
  );
}

export default CustomInput;

Upvotes: 2

Kelvin Schoofs
Kelvin Schoofs

Reputation: 8718

You are only calling buildControls once, where the <input ... gets its value only that single time.

Whenever React re-renders your component (because e.g. some state changes), your {controls} will tell React to render that original <input ... with the old value.

I'm not sure why you are storing your controls in a state variable? There's no need for that, and as you noticed, it complicates things a lot. You would basically require a renderControls() function too that you would replace {controls} with.

Upvotes: 1

Related Questions