Home > front end >  React component updated from classes to functions/hooks but state not working
React component updated from classes to functions/hooks but state not working

Time:04-29

I attempted to refactor a React class component (fahrenheit/celcius temperature converter) to functional components with hooks (in NextJS) but these fields are not updating state as expected when numbers are added to either TemperatureInput field. Nor is BoilingVerdict updating when triggered.

I've tried many variations to the change handles but can't find what I'm missing.

Could someone please point me on the right track?

Thanks in advance!

Original class component:

const scaleNames = {
  c: 'Celsius',
  f: 'Fahrenheit'
};

function toCelsius(fahrenheit) {
  return (fahrenheit - 32) * 5 / 9;
}

function toFahrenheit(celsius) {
  return (celsius * 9 / 5)   32;
}

function tryConvert(temperature, convert) {
  const input = parseFloat(temperature);
  if (Number.isNaN(input)) {
    return '';
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
}

function BoilingVerdict(props) {
  if (props.celsius >= 100) {
    return <p>The water would boil.</p>;
  }
  return <p>The water would not boil.</p>;
}

class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e) {
    this.props.onTemperatureChange(e.target.value);
  }

  render() {
    const temperature = this.props.temperature;
    const scale = this.props.scale;
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={temperature}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}

class Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
    this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
    this.state = {temperature: '', scale: 'c'};
  }

  handleCelsiusChange(temperature) {
    this.setState({scale: 'c', temperature});
  }

  handleFahrenheitChange(temperature) {
    this.setState({scale: 'f', temperature});
  }

  render() {
    const scale = this.state.scale;
    const temperature = this.state.temperature;
    const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
    const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;

    return (
      <div>
        <TemperatureInput
          scale="c"
          temperature={celsius}
          onTemperatureChange={this.handleCelsiusChange} />
        <TemperatureInput
          scale="f"
          temperature={fahrenheit}
          onTemperatureChange={this.handleFahrenheitChange} />
        <BoilingVerdict
          celsius={parseFloat(celsius)} />
      </div>
    );
  }
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Calculator />);

Same component converted to functional components/hooks (but not working):

import React, { useState } from "react";

const scaleNames = {
    c: "Celsius",
    f: "Fahrenheit",
};

const toCelsius = (fahrenheit) => {
    return ((fahrenheit - 32) * 5) / 9;
};

const toFahrenheit = (celsius) => {
    return (celsius * 9) / 5   32;
};

const tryConvert = (temperature, convert) => {
    const input = parseFloat(temperature);
    if (Number.isNaN(input)) {
        return "";
    }
    const output = convert(input);
    const rounded = Math.round(output * 1000) / 1000;
    return rounded.toString();
};

const BoilingVerdict = (props) => {
    if (props.celsius >= 100) {
        return <p>The water would boil.</p>;
    }
    return <p>The water would not boil.</p>;
};

const TemperatureInput = (props) => {
    const handleChange = () => {
        props.onTemperatureChange();
    };

    const temperature = props.temperature;
    const scale = props.scale;
    return (
        <fieldset>
            <legend>Enter temperature in {scaleNames[scale]}:</legend>
            <input value={temperature} onChange={handleChange} />
        </fieldset>
    );
};

export default function Temp(props) {
    const [state, setState] = useState({ temperature: "", scale: "c" });

    const handleCelsiusChange = (temperature) => {
        setState({ scale: "c", temperature });
    };

    const handleFahrenheitChange = (temperature) => {
        setState({ scale: "f", temperature });
    }

    const scale = state.scale;
    const temperature = state.temperature;
    const celsius =
        scale === "f"
            ? tryConvert(temperature, toCelsius())
            : temperature;
    const fahrenheit =
        scale === "c"
            ? tryConvert(temperature, toFahrenheit())
            : temperature;

    return (
        <div>
            <TemperatureInput
                scale="c"
                temperature={celsius}
                onTemperatureChange={handleCelsiusChange}
            />
            <TemperatureInput
                scale="f"
                temperature={fahrenheit}
                onTemperatureChange={handleFahrenheitChange}
            />
            <BoilingVerdict celsius={parseFloat(celsius)} />
        </div>
    );
}

The original is from the React Docs site example on Lifting State, pen here: https://codepen.io/gaearon/pen/WZpxpz?editors=0010

Thanks again!

CodePudding user response:

You had two little errors in your code:

  1. input onChange emits an event. You can get the actual value that was put in by accessing e.target.value.

Without this change, the temperature is and stays undefined, since handleChange in your handleTemperatureChange-Handlers in Temp are called without any parameters.

const TemperatureInput = (props) => {
  const handleChange = (e) => {
    props.onTemperatureChange(e.target.value);
  };

  const temperature = props.temperature;
  const scale = props.scale;
  return (
    <fieldset>
      <legend>Enter temperature in {scaleNames[scale]}:</legend>
      <input value={temperature} onChange={(e) => handleChange(e)} />
    </fieldset>
  );
};
  1. Your calls to tryConvert were incorrect.

    tryConvert(temperature, toCelsius())

In this case the second parameter is not a function but its return value. If you want to feed a function to tryConvert add only the function name:

tryConvert(temperature, toCelsius)

Putting this together the code works fine

import React, { useState } from "react";

const scaleNames = {
  c: "Celsius",
  f: "Fahrenheit"
};

const toCelsius = (fahrenheit) => {
  return ((fahrenheit - 32) * 5) / 9;
};

const toFahrenheit = (celsius) => {
  return (celsius * 9) / 5   32;
};

const tryConvert = (temperature, convert) => {
  const input = parseFloat(temperature);
  if (Number.isNaN(input)) {
    return "";
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
};

const BoilingVerdict = (props) => {
  if (props.celsius >= 100) {
    return <p>The water would boil.</p>;
  }
  return <p>The water would not boil.</p>;
};

const TemperatureInput = (props) => {
  const handleChange = (e) => {
    props.onTemperatureChange(e.target.value);
  };

  const temperature = props.temperature;
  const scale = props.scale;
  return (
    <fieldset>
      <legend>Enter temperature in {scaleNames[scale]}:</legend>
      <input value={temperature} onChange={(e) => handleChange(e)} />
    </fieldset>
  );
};

export default function Temp(props) {
  const [state, setState] = useState({ temperature: "", scale: "c" });

  const handleCelsiusChange = (temperature) => {
    console.log(temperature)
      setState({ scale: "c", temperature });
  };

  const handleFahrenheitChange = (temperature) => {
      setState({ scale: "f", temperature });
  }

  const scale = state.scale;
  const temperature = state.temperature;
  
  const celsius =
      scale === "f"
          ? tryConvert(temperature, toCelsius())
          : temperature;
  const fahrenheit =
      scale === "c"
          ? tryConvert(temperature, toFahrenheit)
          : temperature;

  return (
      <div>
          <TemperatureInput
              scale="c"
              temperature={celsius}
              onTemperatureChange={handleCelsiusChange}
          />
          <TemperatureInput
              scale="f"
              temperature={fahrenheit}
              onTemperatureChange={handleFahrenheitChange}
          />
          <BoilingVerdict celsius={parseFloat(celsius)} />
      </div>
  );
}
  • Related