React, can't set state based on current state(state one step behind)

Viewed 118

I'm absolutely lost with React async setState :(

codesandbox

Here's what I'm trying to achieve: when I click operator button (App.js line 81-98) , I take current(new\updated) state.input, 'format' it with my fixInput function (App.js line 7-22, it takes state.input and event.target.textContent, returns new string), and set the result as new state.input value.

//App.js
function fixInput(inputStr, currentOp) {
  const lastTwoOp = /([+\-*/]{2})$/;
  const lastThreeOp = /([+\-*/]{3})$/; //
  const allowTwo = /(\*|\/)-/; //don't replace *- and /-
  //this  is not  working correctly
  if (lastTwoOp.test(inputStr) && !allowTwo.test(inputStr)) {
    // if there are 2 operators and those are not the allowed combinations, replace  them with the last operator
    inputStr = inputStr.slice(0, -2) + currentOp;
  } else if (lastThreeOp.test(inputStr)) {
    // if there are 3 operators, replace  them with the last operator
    inputStr = inputStr.slice(0, -3) + currentOp;
  } else {
    inputStr += currentOp;
  }
  return inputStr;
}

const init = () => ({
  input: "",
  output: 0,
  showInput: false,
  decimal: false,
  clickedOp: ""
});

export default class App extends Component {
  state = init();

  // HANDLERS
  handleCeClick = () => {
    this.setState(init);
  };

  handleNumClick = (e) => {
    const { input } = this.state;
    const value = e.target.textContent;
    if (e.target.id === "zero" && +input === 0) {
      //if there's 0 in input and 0 is clicked, do nothing
      e.preventDefault();
    } else {
      //add number to input,showinput, reset operators number
      this.setState((state) => ({
        input: input + value,
        showInput: true,
        opnum: 0
      }));
    }
  };

  handleDecClick = (e) => {
    const value = e.target.textContent;
    const { input, decimal } = this.state;
    if (decimal === true) {
      //if there's decimal in the input, do nothing
      e.preventDefault();
    } else {
      //add decimal to the input
      if (!input) {
        this.setState({
          input: "0."
        });
      } else {
        this.setState({
          input: input + value
        });
      }
      //default behavior: set decimal to true and show the input
      this.setState({
        decimal: true,
        showInput: true
      });
    }
  };

  handleOperClick = (e) => {
    const value = e.target.textContent;
    const fixedInput = fixInput(this.state.input, value);

    this.setState(
      (state, props) => {
        return {
          input: fixedInput,
          decimal: false,
          showInput: true,
          latestOper: value
        };
      },
      () => {
        console.log(this.state.input); //current state here  i want to  use this value in fixInput function and  set as  new input state:/
      }
    );
  };

  handleEqualsClick = () => {
    const { input } = this.state;
    //todo ? if the last char is oper, delete it
    // console.log(input);
    let result = +eval(input).toFixed(7).toString();

    if (result.length > 11) {
      result = result.slice(0, 11);
    }
    this.setState((state) => ({
      showInput: false,
      output: result,
      input: result //test14
    }));
  };

  render() {
    const handlers = {
      handleCeClick: this.handleCeClick,
      handleNumClick: this.handleNumClick,
      handleOperClick: this.handleOperClick,
      handleEqualsClick: this.handleEqualsClick,
      handleDecClick: this.handleDecClick,
      handleInput: this.handleInput
    };
    const { input, output, showInput } = this.state;
    return (
      <>
        <Display output={output} input={input} showInput={showInput} />
        <Keypad {...handlers} />
      </>
    );
  }
}
//Display.js
const Display = (props) => {
  //on equals  click  show  the  output ,  if  input continues  , show input, change showoutput
  let { input, output, showInput } = props;

  return (
    <div id="display" className="display_font">
      {showInput ? input : output}
    </div>
  );
};
//Keypad.js
const Keypad = (props) => {
  return (
    <div id="keypad">
      <button id="clear" onClick={props.handleCeClick}>
        CE
      </button>
      <button id="seven" onClick={props.handleNumClick}>
        7
      </button>
      <button id="eight" onClick={props.handleNumClick}>
        8
      </button>
      <button id="nine" onClick={props.handleNumClick}>
        9
      </button>
      <button id="add" onClick={props.handleOperClick}>
        +
      </button>
      <button id="four" onClick={props.handleNumClick}>
        4
      </button>
      <button id="five" onClick={props.handleNumClick}>
        5
      </button>
      <button id="six" onClick={props.handleNumClick}>
        6
      </button>
      <button id="subtract" onClick={props.handleOperClick}>
        -
      </button>
      <button id="one" onClick={props.handleNumClick}>
        1
      </button>
      <button id="two" onClick={props.handleNumClick}>
        2
      </button>
      <button id="three" onClick={props.handleNumClick}>
        3
      </button>
      <button id="multiply" onClick={props.handleOperClick}>
        *
      </button>
      <button id="zero" onClick={props.handleNumClick}>
        0
      </button>
      <button id="decimal" onClick={props.handleDecClick}>
        .
      </button>
      <button id="equals" onClick={props.handleEqualsClick}>
        =
      </button>
      <button id="divide" onClick={props.handleOperClick}>
        /
      </button>
    </div>
  );
};

React setState is async, and it's not updating immediately, so i can access only previous state.input, i can log to console current state from setState callback, and i need to set a new state, which I cannot do from callback. i can't access current state, that is why my fixInput function is not working correctly( press number and repetitive operators, eg 4++, it was expected to allow to add only two combinations of operators *- and /-

I did try to move things around in every way I could think of.

What's wrong with my approach? Is this possible with the structure i've got?

2 Answers

enter image description here

Everything seems okay to me, just sanitize the input equation according to the given specification and all the test cases will be fulfilled.

Make these changes to handleEqualClick() function:

  handleEqualsClick = () => {
    const { input } = this.state;
    //todo ? if the last char is oper, delete it
    // console.log(input);
    let eq = [];
    for (let ch of [...input]) {
      if (!eq.length) {
        eq.push(ch);
      } else {
        if (ch === "-") {
          eq.push("-");
        } else if ("+-*/".includes(ch) && "+-*/".includes(eq[eq.length - 1])) {
          while ("+-*/".includes(ch) && "+-*/".includes(eq[eq.length - 1])) {
            eq.pop();
          }
          eq.push(ch);
        } else {
          eq.push(ch);
        }
      }
    }
    let sanitizedInput = eq.join("");
    console.log(sanitizedInput);
    let result = +eval(sanitizedInput).toFixed(7).toString();
    console.log("result:", result);

    if (result.length > 11) {
      result = result.slice(0, 11);
    }
    this.setState((state) => ({
      showInput: false,
      output: result,
      input: result //test14
    }));
  };

Codesandbox Link

Some clarifications: I wanted 1) to pass test #13, 2) see correct characters on display

Ketan Ramteke solution is working for the test but not for characters. There was a mistake in fixInput function, I tested this.state.input when I needed the updated version which is this.state.input + value.

function fixInput(inputStr, currentOp) {
    const lastTwoOp = /([+\-*/]{2})$/;
    const lastThreeOp = /([+\-*/]{3})$/;
    const allowTwo = /(\*|\/)-/;

    const toTest = inputStr + currentOp; //<= 

    if (lastTwoOp.test(toTest) && !allowTwo.test(toTest)) {
        inputStr = inputStr.slice(0, -1) + currentOp;
    } else if (lastThreeOp.test(toTest)) {
        inputStr = inputStr.slice(0, -2) + currentOp;
    } else {
        inputStr += currentOp;
    }
    return inputStr;
}
Related