Search code examples
javascriptreactjstypescriptreact-nativereact-props

unable to type in react input field


Currently, the default value of my input field is 1. If I try to type something in the input field, nothing changes.

interface Orders {
    order_graph_1: number;
    order_graph_2: number;
  }

  interface MyProps extends Orders {
    setOrders: (...args: any) => void; // function which takes args...??
  }

  interface MyState extends Orders {
    //otherProperty: string;
  }
  
      

class Setup extends React.Component<MyProps, MyState>{
    state = {
        order_graph_1: this.props.order_graph_1,
        order_graph_2: this.props.order_graph_2
      };
    
      // needs to be an arrow function to access `this` properly
      // could use ( event: React.ChangeEvent<HTMLInputElement>)
      // could avoid the assertion by passing the name as an argument
      setOrders = (event: any) => {

        this.setState((prevState) => ({
          ...prevState,
          [event.target.name]: parseInt(event.target.value)
        }));
      };
    render(){
        return(
            <div className="row">
                <div className="col-6">
                    <p className="text-center">Order of first model: </p>
                    <div className="w-100 d-flex justify-content-center">
                        <input className="text-center" name="order_graph_1" type="number" value={this.props.order_graph_1} onChange={this.setOrders.bind(this)} min="1" max="10"/>
                </div>
                </div>
            </div>
        );
    }
}

export default Setup;

To test, I canged the onChange function

onChange={()=>console.log("hello")}

everytime I tried to type in the input field, I saw hello being printed in the console but the value of the input field still does not change.

edit:

This was a JS code (https://github.com/MenesesGHZ/polynomial-regression-js):

class RegressionSetup extends React.Component{
    constructor(props){
        super(props);
        this.orders = {
            "order_graph_1":this.props.order_graph_1,
            "order_graph_2":this.props.order_graph_2
        }; 
    }
    
    setOrders(event){
        this.orders[event.target.name] = parseInt(event.target.value);
        this.props.setOrders(Object.values(this.orders));
    }

    render(){
        return(
            <div className="row">
                <div className="col-6">
                    <p className="text-center">Order of first model: </p>
                    <div className="w-100 d-flex justify-content-center">
                        <input className="text-center" name="order_graph_1" type="number" value={this.props.order_graph_1} onChange={this.setOrders.bind(this)} min="1" max="10"/>
                </div>
                </div>
                <div className="col-6">
                    <p className="text-center">Order of second model: </p>
                    <div className="w-100 d-flex justify-content-center">
                        <input className="text-center"name="order_graph_2" type="number" value={this.props.order_graph_2} onChange={this.setOrders.bind(this)} min="1" max="10"/>
                    </div>
                </div>
            </div>
        );
    }
}

export default RegressionSetup;

Upon changing the value of input, a line on a graph changed according to the value. I had to change this code to Typescript. This is what I have now.

interface Orders {
    order_graph_1: number;
    order_graph_2: number;
  }

  interface MyProps extends Orders {
    setOrders: (...args: any) => void; // function which takes args...??
  }

  interface MyState extends Orders {
    //otherProperty: string;
  }
  
      

class Setup extends React.Component<MyProps, MyState>{
    state = {
        // it is best not to derive state from props
        order_graph_1: this.props.order_graph_1,
        order_graph_2: this.props.order_graph_2
      };
    
      // needs to be an arrow function to access `this` properly
      // could use ( event: React.ChangeEvent<HTMLInputElement>)
      // could avoid the assertion by passing the name as an argument
      setOrders = (event: any) => {
        // I don't love this solution, but we can avoid the TS errors by copying the previous state
        this.setState((prevState) => ({
          ...prevState,
          [event.target.name]: parseInt(event.target.value)
        }));
      };
    render(){
        return(
            <div className="row">
                <div className="col-6">
                    <p className="text-center">Order of first model: </p>
                    <div className="w-100 d-flex justify-content-center">
                        <input className="text-center" name="order_graph_1" type="number" value={this.state.order_graph_1} onChange={this.setOrders.bind(this)} min="1" max="10"/>
                </div>
                </div>
                {/* <div className="col-6">
                    <p className="text-center">Order of second model: </p>
                    <div className="w-100 d-flex justify-content-center">
                        <input className="text-center"name="order_graph_2" type="number" value={this.props.order_graph_2} onChange={this.setOrders.bind(this)} min="1" max="10"/>
                    </div>
                </div> */}
            </div>
        );
    }
}

export default Setup;   

although it compiles without an error, the input value thing is not working. It does not change the line on the graph so I am assuming the state is not saved. How can I fix this?


Solution

  • The problem is that you are using the value of order_graph_1 and order_graph_2 that you get from props but updating the ones that you have in state and not updating the ones in the props.

    The code that you are converting updates both the state and the props (by calling this.props.setOrders). But it is totally unnecessary to have these variables in a component state of RegressionSetup at all since it can already access and update them through props.

    The setOrders function which is passed down from the parent Main component is also a bit silly because both components have a state with properties order_graph_1 and order_graph_2 -- so why are we passing them as a tuple to setOrders?

    In Main you can delete the setOrders function and instead pass down an arrow function that calls setState.

    <RegressionSetup
      order_graph_1={this.state.order_graph_1}
      order_graph_2={this.state.order_graph_2}
      setOrders={(orders) => this.setState(orders)}
    />
    

    setState in a class component takes a partial new state and merges it with the current state. That means the we don't even have to pass both orders to setOrders. We can call it with just one order and that's fine.

    So we can define the props for our RegressionComponent like this:

    export interface OrderGraphs {
      order_graph_1: number;
      order_graph_2: number;
    }
    
    interface Props extends OrderGraphs {
      setOrders(graphs: Partial<OrderGraphs>): void;
    }
    

    There is no reason for it to be a class component, but it also doesn't matter. RegressionSetup does not need to use state, hooks, or lifecycle methods. It just takes props and returns JSX.

    I still prefer to render the two inputs using a function since it's less repetition but obviously you don't have to do that. We use the variable property to get the correct property from props value={this.props[property]} and to set the correct key on the object that we pass to setOrders: onChange={(e) => this.props.setOrders({[property]: parseInt(e.target.value, 10)})}

    class RegressionSetup extends React.Component<Props> {
      renderInput(property: keyof OrderGraphs, label: string) {
        return (
          <div className="col-6">
            <p className="text-center">{label}</p>
            <div className="w-100 d-flex justify-content-center">
              <input
                className="text-center"
                name={property}
                type="number"
                value={this.props[property]}
                onChange={(e) =>
                  this.props.setOrders({ [property]: parseInt(e.target.value, 10) })
                }
                min="1"
                max="10"
                step="1"
              />
            </div>
          </div>
        );
      }
    
      render() {
        return (
          <div className="row">
            {this.renderInput("order_graph_1", "Order of first model: ")}
            {this.renderInput("order_graph_2", "Order of second model: ")}
          </div>
        );
      }
    }
    

    Code Sandbox Link

    I messed with the other components as well. I added Props and State types for everything. I also used arrow functions to get rid of all the bind(this) calls. There are still a few Typescript errors related to the external packages (chart.js and mathjs).