Search code examples
javascriptreactjsweather-api

Toggle language using React without Redux


I'm trying to create a weather app in React. I want to toggle the app language using onClick.

Here's my code so far.

import React, { Component } from "react";
import axios from "axios";
import "./App.css";

class App extends Component {

componentDidMount() {
    this.setState({
      isLoading: true
    });
    axios
      .get("path to weather api")
      .then(res => {
        console.log(res.data.data[0]);
        const { city_name, temp, weather } = res.data.data[0];
        this.setState({
          loc: city_name,
          temp: temp,
          code: weather.code,
          isLoading: false
        });
        this.setState({
          desc: this.convertCode(this.state.code)
        });
      });
  }

switchLanguage = () => {
    if (this.state.lang === "en") {
      this.setState({
        lang: "hi",
        desc: this.convertCode(this.state.code)
      });
    } else {
      this.setState({
        lang: "en",
        desc: this.convertCode(this.state.code)
      });
    }
  };

convertCode = givenCode => {
    if (this.state.lang === "en") {
      if (
        givenCode === 200 ||
        givenCode === 201 ||
        givenCode === 202 ||
        givenCode === 230 ||
        givenCode === 231 ||
        givenCode === 232 ||
        givenCode === 233 ||
        givenCode === "200" ||
        givenCode === "201" ||
        givenCode === "202" ||
        givenCode === "230" ||
        givenCode === "231" ||
        givenCode === "232" ||
        givenCode === "233"
      ) {
        return "Thunderstorms";
      } else if (
        givenCode === 300 ||
        givenCode === 301 ||
        givenCode === 302 ||
        givenCode === "300" ||
        givenCode === "301" ||
        givenCode === "302"
      ) {
        return "Drizzle";
      }
      ..............
      ..............
      IF CONDITION FOR THE OTHER LANGUAGE
  };

render() {
    if (!this.state.isLoading) {
      return (
        <div className="App">
          <div className="container">
            <div className="languageSwitcher">
              <i className="fa fa-language" onClick={this.switchLanguage} />
            </div>
            <div className="location">
              <i className="fa fa-location-arrow" /> {this.state.loc}
            </div>
            {this.state.lang === "en" && (
              <div className="temperature">It's {this.state.temp} degrees.</div>
            )}
            {this.state.lang === "hi" && (
              <div className="temperature">
                तापमान {this.state.temp} डिग्री है।
              </div>
            )}
            <div className="description">{this.state.desc}</div>
          </div>
        </div>
      );
    } else {
      return <div className="loading">Fetching weather data...</div>;
    }
  }
}

export default App;

Everything works except for the div with className="desc". desc is always one phase behind. I mean when state.lang is en, it displays the text in hi and vice-versa.

I just started learning React so the code's pretty messed up. Sorry about that.

Thanks.


Solution

  • You have two state management issues and one flow-of-execution issue, one (or possibly more) of which is causing the behavior you mention (but all need fixing in any case):

    1. State updates are asynchronous. This means that this.state does not have the updated state immediately after a this.setState call.

    2. Because state updates are asynchronous, if you're setting state based on existing state (which you are in a few places, including switchLanguage), you must use the version of setState you pass a callback to, not the version you pass an object to; in the callback, use the up-to-date state object the callback receives as a parameter.

    3. When you do this.setState({/*...*/desc: this.convertCode(/*...*/)}), you're calling convertCode before calling setState, and passing its return value into setState as a value on a property on the object you're passing it. So even if it weren't for issue #2 above, it would still have a basic flow-of-control issue and convertCode would still see the about-to-become-out-of-date this.state.lang.

    The best way to address all this is probably to update convertCode to optionally accept the lang to use (defaulting to this.state.lang):

    convertCode = (givenCode, lang = this.state.lang) => {
        // ...use `lang`, not `this.state.lang`...
    

    ...and then address the various issues setting state and using convertCode. The first is in componentDidMount:

    componentDidMount() {
        this.setState({
          isLoading: true
        });
        axios
          .get("path to weather api")
          .then(res => {
            console.log(res.data.data[0]);
            const { city_name, temp, weather } = res.data.data[0];
            this.setState({
              loc: city_name,
              temp: temp,
              code: weather.code,
              isLoading: false
            });
            this.setState({
              desc: this.convertCode(this.state.code) // <=== Error is here
            });
          });
      }
    

    this.state.code won't have been updated yet, because state updates are asynchronous. Also, we want to use this.state.lang, so we need to use the callback form. Instead, combine those two calls and pass the lang to convertCode:

            this.setState(prevState => ({
              loc: city_name,
              temp: temp,
              code: weather.code,
              isLoading: false,
              desc: this.convertCode(weather.code, prevState.lang),
            }));
    

    In switchLanguage, both problems #2 and #3 exist:

    // INCORRECT:
    //   A) Sets state based on state without callback
    //   B) Calls `convertCode` before `setState`
    switchLanguage = () => {
        if (this.state.lang === "en") {
          this.setState({
            lang: "hi",
            desc: this.convertCode(this.state.code)
          });
        } else {
          this.setState({
            lang: "en",
            desc: this.convertCode(this.state.code)
          });
        }
      };
    

    We can fix both issues by using the callback form and passing the language to use into convertCode:

    // Uses callback when setting state based on state
    switchLanguage = () => {
        this.setState(prevState => {
          const lang = prevState.lang === "en" ? "hi": "en";
          return {lang, desc: this.convertCode(prevState.code, lang)};
        });
      };
    

    Notice the use of prevState both for the lang check and for passing code to this.convertCode.