Ruham
Ruham

Reputation: 769

React Parent component checkbox state updates with one step delay

I have a Parent component:

import React, { Component } from "react";
import { Button } from "./Button";

export class Dashboard extends Component {
  constructor(props) {
    super(props);
    this.state = {
      numbers: [],
      disabled: false
    };

    this.setNum = this.setNum.bind(this);
  }

  setNum(num) {
    if (!this.state.numbers.includes(num)) {
      this.setState(prevState => ({
        numbers: [...prevState.numbers, num]
      }));
    } else if (this.state.numbers.includes(num)) {
      let nums = [...this.state.numbers];
      let index = nums.indexOf(num);
      nums.splice(index, 1);
      this.setState({ numbers: nums });
      console.log(this.state.numbers);
    }
    if (this.state.numbers.length >= 4) {
      this.setState({ disabled: true });
    } else if (this.state.numbers.length < 4) {
      this.setState({ disabled: false });
    }
  }

  render() {
    return (
      <div className="board-container">
        <div className="board">
          <div className="row">
            <Button
              id="1"
              numbers={this.state.numbers}
              onChange={this.setNum}
              disabled={this.state.disabled}
            />
            <Button
              id="2"
              numbers={this.state.numbers}
              onChange={this.setNum}
              disabled={this.state.disabled}
            />
            <Button
              id="3"
              numbers={this.state.numbers}
              onChange={this.setNum}
              disabled={this.state.disabled}
            />
            <Button
              id="4"
              numbers={this.state.numbers}
              onChange={this.setNum}
              disabled={this.state.disabled}
            />
          </div>
        </div>
      </div>
    );
  }
}

... and a Child component:

import React, { Component } from "react";

export class Button extends Component {
  constructor(props) {
    super(props);
    this.state = {
      isChecked: false
    };

    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e) {
    this.setState({
      isChecked: !this.state.isChecked
    });
    var num = e.target.value;
    this.props.onChange(num);
  }

  render() {
    const { isChecked } = this.state;

    if (isChecked === true) {
      var bgColor = "#f2355b";
    } else {
      bgColor = "#f7f7f7";
    }

    let disabled = this.props.disabled;

    if (this.props.numbers.includes(this.props.id)) {
      disabled = false;
    }

    return (
      <div className="number-container" id="checkboxes">
        <label
          className={!isChecked && disabled === false ? "num" : "checked-num"}
          style={{ backgroundColor: bgColor }}
        >
          {" "}
          {this.props.id}
          <input
            type="checkbox"
            name={this.props.id}
            value={this.props.id}
            id={this.props.id}
            onChange={this.handleChange}
            checked={isChecked}
            disabled={disabled}
          />
        </label>
      </div>
    );
  }
}

Whenever any Button component is clicked, the Parent component gets the child Button's id value and puts it into its numbers state array. Whenever a Button is unchecked, the Parent updates is numbers state by removing the id of the child Button.

If my code is right, the expected behavior is whenever a Button checkbox is clicked, the Parent numbers state will be updated immediately (adding or removing a number). However, it always updates with one step lag behind.

I know, that the issue is dealing with the React states not being updated instantly, and I've checked similar issues on Stackoverflow. The problem is that I can't figure it out how to make this two components interact with each other in a proper way. What would be the solution for this issue?

Upvotes: 1

Views: 1810

Answers (2)

Cagri Yardimci
Cagri Yardimci

Reputation: 3539

enter image description here

enter image description here

enter image description here

Here are three screenshots from codesandbox

If you want to play with it please find the link https://codesandbox.io/s/w2q8ypnxjw

What I did was, I basically copied and pasted your code and updated setNum function to reflect the changes Think-Twice suggested

  setNum(num) {
    if (!this.state.numbers.includes(num)) {
      this.setState(
        prevState => ({
          numbers: [...prevState.numbers, num]
        }),
        () => {
          console.log("state logged inside if", this.state.numbers);
        }
      );
    } else if (this.state.numbers.includes(num)) {
      let nums = [...this.state.numbers];
      let index = nums.indexOf(num);
      nums.splice(index, 1);
      this.setState({ numbers: nums }, () => {
        console.log("state logged inside else if", this.state.numbers);
      });
    }
    if (this.state.numbers.length >= 4) {
      this.setState({ disabled: true });
    } else if (this.state.numbers.length < 4) {
      this.setState({ disabled: false });
    }
  }

So before going further let's quickly address a couple of things regarding to React and setState

  1. As B12Toaster mentioned and provided a link which contains a quote from official documentation

    setState() does not always immediately update the component. It may batch or defer the update until later.

    Think-Twice's also points out that by stating

    Basically setState is asynchronous in React. When you modify a value using setState you will be able to see the updated value only in render..

    So if you want to see the immediate state change in a place which you trigger setState, you can make use of a call back function as such setState(updater[, callback])

  2. There are two approaches when it comes to and updater with setState, you could either pass an object, or you could pass a function So in Think-Twice's example, an object is passed as an updater

    this.setState({ numbers: nums } //updater, () => {
            console.log(this.state.numbers); //this will print the updated value here
      });
    

    When a function is used as an updater (in your setNum function you already do that), the callback function can be utilized like below

    if (!this.state.numbers.includes(num)) {
          this.setState(
            prevState => ({
              numbers: [...prevState.numbers, num]
            }),
            () => {
              console.log("state logged inside if", this.state.numbers);
            }
          );
        }
    

Your current implementation and communication structure seems fine. It is actually called Lifting State Up which is recommended also by official documentation. Basically you store the state of array numbers in a parent component (which can be considered as the source of truth) and you pass the method that changes the state as a prop to it's child component.

In the codesandbox link I provided, the functionalities works the way I expect (at least this is what I expect from your code)

Upvotes: 2

Hemadri Dasari
Hemadri Dasari

Reputation: 34014

Basically setState is asynchronous in React. When you modify a value using setState you will be able to see the updated value only in render. But to see updated state value immediately you need to do something like below

  this.setState({ numbers: nums }, () => {
        console.log(this.state.numbers); //this will print the updated value here
  });

Upvotes: 1

Related Questions