JavaScript Calculator Project - Front End Libraries Projects - Help Needed

Just because the clear test passes doesn’t mean it can’t fail later. The “123” clear test passes but then the next test fails and as you can see the values from the “123” clear test is not reset before the next test runs. It is also when the math library starts to throw (eval would throw as well).

To prove my point, if I replace the map with manually added buttons with an onClick attribute your code passes 14/16 and the last two tests that fail are from incorrect input handling.

Test 12: 10.5 - 5.5 (here your code fails to add the decimal point to the last number)

Test 13: 5 * - + 5 (you code outputs -25 and not 10)

1 Like

@jsdisco I tried making a copy of input into newInput like you said, but it made storedValue which I’m setting to a string version of it to always lag one number or operator behind currentValue which I don’t like. So I put it back to const newInput = input;.

Anyways. With what I have now, I just need 4 of the tests to pass: 9, 11, 12 and 13. The rest are all passing.

9:

9. In any order, I should be able to add, subtract, multiply and divide a chain of numbers of any length, and when I hit "=", the correct result should be shown in the element with the id of "display"
The expression 3 + 5 * 6 - 2 / 4 should produce 32.5 or 11.5 as an
          answer, depending on the logic your calculator uses
          (formula vs. immediate execution) 
AssertionError: The expression 3 + 5 * 6 - 2 / 4 should produce 32.5 or 11.5 as an
          answer, depending on the logic your calculator uses
          (formula vs. immediate execution) 

When I try doing this manually, I get 32.5 as the answer. So I really need to know how I can get this test to pass.

11:

11. When the decimal element is clicked, a "." should append to the currently displayed value; two "." in one number should not be accepted
An input of "5 . 5 . 5" should display 5.55 : expected '55.5' to equal '5.55'
AssertionError: An input of "5 . 5 . 5" should display 5.55 : expected '55.5' to equal '5.55'

Again, trying this manually gives me 5.55 in the display. How do I get this test to pass?

12:

12. I should be able to perform any operation (+, -, *, /) on numbers containing decimal points
The expression "10.5 - 5.5" should produce an output of "5" : expected '5.5' to equal '5'
AssertionError: The expression "10.5 - 5.5" should produce an output of "5" : expected '5.5' to equal '5'

Trying this manually produces 5 as the answer. So how do I get this test to pass?

13:

13. If 2 or more operators are entered consecutively, the operation performed should be the last operator entered (excluding the negative (-) sign.
The sequence "5 * - + 5" = should produce an output of "10" : expected '25' to equal '10'
AssertionError: The sequence "5 * - + 5" = should produce an output of "10" : expected '25' to equal '10'

This is the only one out of these it’s right about. I need help on getting this right and also on getting the test to pass.

GitHub repo is updated.

Please help me out with fixing the problems.

@lasjorg Did you try doing what tests 9, 11 and 12 are doing manually? When I do it I always get the right result. Hence my confusion. I also tried doing “123” and then clearing the display. No weird issues here. I should probably try logging the input array, but what handler(s) should I do it in?

You can not rely on manually testing it when it comes to bugs caused by the automatic test that you simply won’t see when doing manual input. The speed at which the tests run can cause issues you won’t see when doing manual input testing.

Like I showed, your code is not failing in the same way when I switch how the event handling is done. Then it only fails the tests you are not logically handling correctly.


Edit: Again, I will reiterate this point one last time. I suggest you switch how you do the event handling. If you absolutely must use a map for the buttons then maybe try adding the handler function names to the button objects in the array and pass the functions down as props to the buttons and use them on an onClick attribute. Then you can remove the event listener on the keypad and the event delegation function.

As for the last two failing tests, making them pass is part of the assignment, if you have questions regarding something specific you can ask, but you can not just say “please help me pass the tests”.

1 Like

Alright, now all except one of the tests pass after I did what you suggested about the onClick attribute. I moved the definition of the buttons array to after all of the click handlers and added clickHandler properties to them there, setting them equal to the necessary handler function.

The test that doesn’t pass is this one:

13. If 2 or more operators are entered consecutively, the operation performed should be the last operator entered (excluding the negative (-) sign.
The sequence "5 * - + 5" = should produce an output of "10" : expected '25' to equal '10'
AssertionError: The sequence "5 * - + 5" = should produce an output of "10" : expected '25' to equal '10'

Here’s my operator click handler:

  const handleOperatorClick = event => {
    if (input.length > 0) {
      // handle 2 or more operators clicked in a row
      if (operators.includes(input[input.length - 1]) && event.target.textContent !== "-") {
        // take the previously clicked operator out of the input array
        const newInput = input;
        newInput.splice(-1, 1);
        setInput(newInput);
        setStoredValue(input.join(""));
      } else if (input[input.length - 1].endsWith("^2")) {
        const newInput = input;
        newInput.push(event.target.textContent);
        setInput(newInput);
        setStoredValue(input.join(""));
      }
    }

    // take the result of the previous evaluation for a new calculation
    if (equalsClicked) {
      // This means equals button was clicked (can be like this in other cases too but still)
      // set input array and storedValue to equal the result from the
      // previous calculation
      const newInput = input;
      newInput.push(currentValue);
      setInput(newInput);
      setStoredValue(input.join(""));

      // reset to false to make sure other click handlers don't misunderstand
      setEqualsClicked(false);
    } else if (reciprocalClicked || squareRootClicked) {
      const newInput = input;
      setInput(newInput);
      setStoredValue(input.join(""));

      // reset to false to make sure other click handlers don't misunderstand
      setReciprocalClicked(false);
      setSquareRootClicked(false);
    }

    const newInput = input;
    newInput.push(event.target.textContent);
    setInput(newInput);
    setStoredValue(input.join(""));
  };

What should I do here (in plain English) for the case where there was already an operator in there? I thought just not allowing more than one operator in a row would be fine but I guess that’s not the case?

I also have something weird happening in my sign-switch button handler:

  const handleSignSwitchClick = () => {
    if (Math.sign(Number(currentValue)) === 1) {
      setCurrentValue(`-${currentValue}`);
      const newInput = input;

      // replace existing last element in array with new
      // sign-changed value
      newInput.pop();
      newInput.push(currentValue);
      setInput(newInput);
      setStoredValue(input.join(""));
    } else if (Math.sign(Number(currentValue)) === -1) {
      const positiveNum = Math.abs(Number(currentValue));
      setCurrentValue(positiveNum.toString());
      const newInput = input;

      // replace existing last element in array with new
      // sign-changed value
      newInput.pop();
      newInput.push(currentValue);
      setInput(newInput);
      setStoredValue(input.join(""));
    }
  };

The sign on the number added to storedValue and probably also the input array is the opposite of the on currentValue. What did I do wrong here? Should I manually do the same to storedValue and the string in the last element of the input array that I do to currentValue for this to work?

Edit: Forgot to mention @lasjorg earlier, so doing it now.

For test 13, do I just need to somehow get math.js to not treat the plus sign as the unary “positive sign” operator in that situation? How do I do that, if so?

The sign-switch button handler works properly now, by the way.

That’s not your problem. The problem is this line:
newInput.splice(-1, 1); and the hard-coded parameter -1.
After entering a sequence of 5 * - +, your storedValue logs 5*+. The multiplication operator is still in there, because you haven’t cleared the value to contain only the last entered operator (+ in this case).

And this is just for when you’ve fixed the code to pass all tests… :

That’s horrible practice, in fact it’s totally anti-React. To manage state in a functional component, you have a myState variable and a setMyState function to modify that variable. But you might not realise that you never actually use that function. I can comment out each line with setState(newInput) in your entire App.js and the code still works exactly the same, because you’ve already modified state directly each time you modified the newInput array.
Might not be something that you care about much right now, but if you want to refactor the code after passing all tests, that’s the first thing I’d address.

Alright, thanks. What about if I do newInput.splice(-i, i)?

For the input and setInput problem: how can I have that array get all of my input as I enter input into the calculator after I fix that issue? Is there a way to not have the array lag behind what I’m doing? Or do I have no choice but to write code in the equals click handler to put currentValue into the array and storedValue before doing the calculation?

This isn’t really something I want to bother with after passing all tests. I’d rather get it out of the way now if it’s that big of a problem.

Edit: For my multiple operators in a row issue: I was thinking of writing a loop to check how many operators there are before the current one that was clicked on and removing them. Is there an easy way to do this?

I changed the code to store a copy of input in newInput by changing const newInput = input to const newInput = [...input].

But now I see an issue in handleEqualsClick where even after the input array has taken all of my input, the storedValue string is still an incomplete expression as it ends with an operator (when I do 3 + 5 * 6 - 2 / 4; storedValue ends with / even though input has the whole thing and I did do setStoredValue(input.join("")).

I fixed that by writing the handler like this:

  const handleEqualsClick = event => {
    setEqualsClicked(true);

    const config = {
      epsilon: 1e-12,
      matrix: "Matrix",
      number: "number",
      precision: 64,
      predictable: false,
      randomSeed: null
    };
    const math = create(all, config);

    const newInput = [...input];
    newInput.push(currentValue);
    setInput(newInput);
    setStoredValue(input.join(""));
    let stored = "";
    if (math.parse(storedValue) !== null) {
      stored = storedValue.concat(currentValue);
    }

    try {
      const calculatedValue = math.round(1000000000000 * math.evaluate(stored)) / 1000000000000;
      setCurrentValue(`${calculatedValue}`);
    } catch (err) {
      console.log(`Error occurred: ${err}`);
    }

    newInput.length = 0;
    setInput(newInput);
    setStoredValue(`${stored}${event.target.textContent}`);
  };

But I still have an issue with decimal numbers; when I try to do 10.5 - 5.5 and then click =, another .5 gets added to the storedValue string. Now how do I fix this?

Is there no “React-friendly” way to have the input array and storedValue update at the same time as me giving input to the calculator? If there is I’d rather just do that instead of all this.

Edit for info: Three of the tests currently fail:

12. I should be able to perform any operation (+, -, *, /) on numbers containing decimal points
The expression "10.5 - 5.5" should produce an output of "5" : expected '5.5' to equal '5'
AssertionError: The expression "10.5 - 5.5" should produce an output of "5" : expected '5.5' to equal '5'

13. If 2 or more operators are entered consecutively, the operation performed should be the last operator entered (excluding the negative (-) sign.
The sequence "5 * - + 5" = should produce an output of "10" : expected '-25' to equal '10'
AssertionError: The sequence "5 * - + 5" = should produce an output of "10" : expected '-25' to equal '10'

14. Pressing an operator immediately following "=" should start a new calculation that operates on the result of the previous evaluation
The sequence "5 - 2 = / 2 =" should produce an output of "1.5" : expected '2' to equal '1.5'
AssertionError: The sequence "5 - 2 = / 2 =" should produce an output of "1.5" : expected '2' to equal '1.5'

Some help and/or hints would be great. Thanks.

Full code repo again.

@jsdisco @lasjorg Why doesn’t this remove all of the operators from the newInput array?

while (operators.includes(newInput[newInput.length - 1])) {
  newInput.pop();
}

I’m doing this before I add the newly clicked operator to newInput.

I also need help with getting it to start a new calculation using the result of the previous evaluation when an operator is clicked after = was clicked. Right now when I try to do this, storedValue becomes empty as soon as an operator is clicked.

Here is my handleOperatorClicked function:

  const handleOperatorClick = event => {
    if (input.length > 0) {
      // handle 2 or more operators clicked in a row
      const newInput = [...input];
      if (operators.includes(input[input.length - 1]) && event.target.textContent !== "-") {
        // take the previously clicked operator(s) out of the input array
        while (operators.includes(newInput[newInput.length - 1])) {
          newInput.pop();
        }
        newInput.push(event.target.textContent);
        setInput(newInput);
        console.log(input);
        setStoredValue(input.join(""));
        let stored = "";
        if (input !== storedValue.split("")) {
          for (let i = 0; i < input.length; i++) {
            stored = `${stored}${input[i]}`;
            if (input === storedValue.split("")) {
              break;
            }
          }
        }
        setStoredValue(stored);
      } else if (input[input.length - 1].endsWith("^2")) {
        const newInput = [...input];
        newInput.push(event.target.textContent);
        setInput(newInput);
        setStoredValue(input.join(""));
        let stored = "";
        if (input !== storedValue.split("")) {
          for (let i = 0; i < input.length; i++) {
            stored = `${stored}${input[i]}`;
            if (input === storedValue.split("")) {
              break;
            }
          }
        }
        setStoredValue(stored);
      }
    }

    // take the result of the previous evaluation for a new calculation
    if (equalsClicked) {
      // This means equals button was clicked (can be like this in other cases too but still)
      // set input array and storedValue to equal the result from the
      // previous calculation
      const newInput = [...input];
      newInput.push(currentValue);
      newInput.push(event.target.textContent);
      setInput(newInput);
      console.log(input);
      setStoredValue(input.join(""));
      let stored = "";
      if (input !== storedValue.split("")) {
        for (let i = 0; i < input.length; i++) {
          stored = `${stored}${input[i]}`;
          if (input === storedValue.split("")) {
            break;
          }
        }
      }
      setStoredValue(stored);

      // reset to false to make sure other click handlers don't misunderstand
      setEqualsClicked(false);
    } else if (reciprocalClicked || squareRootClicked) {
      const newInput = [...input];
      setInput(newInput);
      setStoredValue(input.join(""));

      let stored = "";
      if (input !== storedValue.split("")) {
        for (let i = 0; i < input.length; i++) {
          stored = `${stored}${input[i]}`;
          if (input === storedValue.split("")) {
            break;
          }
        }
      }
      setStoredValue(stored);

      // reset to false to make sure other click handlers don't misunderstand
      setReciprocalClicked(false);
      setSquareRootClicked(false);
    }

    const newInput = [...input];
    newInput.push(event.target.textContent);
    setInput(newInput);
    setStoredValue(input.join(""));

    let stored = "";
    if (input !== storedValue.split("")) {
      for (let i = 0; i < input.length; i++) {
        stored = `${stored}${input[i]}`;
        if (input === storedValue.split("")) {
          break;
        }
      }
    }
    setStoredValue(stored);
  };

@jsdisco @lasjorg I just tried this (complete operator handler function):

  const handleOperatorClick = event => {
    if (input.length > 0) {
      // handle 2 or more operators clicked in a row
      let newInput = [...input];
      if (operators.includes(input[input.length - 1]) && event.target.textContent !== "-") {
        // take the previously clicked operator(s) out of the input array
        newInput = newInput.filter(elem => !operators.includes(elem));
        newInput.push(event.target.textContent);
        setInput(newInput);
        console.log(input);
        console.log(newInput);
        setStoredValue(newInput.join(""));
      } else if (operators.includes(input[input.length - 1]) && event.target.textContent === "-") {
        const newInput = [...input];
        let length = newInput.length;
        let i = newInput.length - 2;
        while (length--) {
          if (operators.includes(newInput[i])) {
            newInput.splice(i, 1);
          }
          i--;
        }
        newInput.push(event.target.textContent);
        console.log(newInput);
        setInput(newInput);
        setStoredValue(newInput.join(" "));
      } else if (input[input.length - 1].endsWith("^2")) {
        const newInput = [...input];
        newInput.push(event.target.textContent);
        setInput(newInput);
        setStoredValue(newInput.join(""));
      }
    }

    // take the result of the previous evaluation for a new calculation
    if (equalsClicked) {
      // This means equals button was clicked (can be like this in other cases too but still)
      // set input array and storedValue to equal the result from the
      // previous calculation
      const newInput = [...input];
      newInput.push(currentValue);
      newInput.push(event.target.textContent);
      setInput(newInput);
      console.log(newInput);
      setStoredValue(newInput.join(""));

      // reset to false to make sure other click handlers don't misunderstand
      setEqualsClicked(false);
    } else if (reciprocalClicked || squareRootClicked) {
      const newInput = [...input];
      setInput(newInput);
      setStoredValue(newInput.join(""));

      // reset to false to make sure other click handlers don't misunderstand
      setReciprocalClicked(false);
      setSquareRootClicked(false);
    }

    const newInput = [...input];
    newInput.push(event.target.textContent);
    setInput(newInput);
    setStoredValue(newInput.join(""));
  };

newInput is updated like this too, so I’ll keep it like this.

But yeah, why isn’t setInput working correctly here? What am I doing wrong?

I just found this page. On updating state initialized with useState.

But it doesn’t mention how to remove stuff from an array in state or how to empty it?

And if I do this

      if (operators.includes(input[input.length - 1]) && event.target.textContent !== "-") {
        // take the previously clicked operator(s) out of the input array
        // and add in newly clicked one
        setInput(input.filter(elem => !operators.includes(elem)), event.target.textContent);
        setStoredValue(input.join(""));
      }

For when the operator just clicked isn’t -, what should I do for when it is? How do I remove just the elements up until the last one and then add the - in?

Wow, @jsdisco and @lasjorg, you guys are heros! How do you have time to review all this? My eyes burned out halfway through.

@DragonOsman, if you want to compare to my project you can. https://codesandbox.io/s/56wgz?file=/src/App.jsx

As someone suggested, Codesandbox has definitely been a better environment than the standard freeCodeCamp setup when working with React.

I’m back at the stage you were when it came to a test-passing Clear button. I’m going to re-read the advice to see if I’ll need to unmap everything or if I have some error in my props passing.

I noticed that you had some decimal issues. Don’t know if you still do because I couldn’t continue reading this all in one sitting. In my case, I added an additional state to see if a decimal was present, I setDecimal when used, then made a conditional statement like this: !decimalOn && setNumber(number + ".");

It’s an interesting project (I made a really fancy calculator with JavaScript once), and reviewing code in React helps me learn. It does get a bit much though now…
But I’m this :pinching_hand: close to trying that challenge myself.

Because it seems there’s a few very basic things about React that are a mystery to me, for example the following snippet (from the handleOperatorClick function). I entered 5 * and I really don’t understand the logs from the * click:

const handleOperatorClick = (event) => {
    const newInput = [...input];

    console.log("input 1", input);       // ['5']
    console.log("newInput 1", newInput); // ['5', '*']

    newInput.push(event.target.textContent);
    setInput(newInput); // Why doesn't this update the input state?

    console.log("input 2", input);       // ['5']
    console.log("newInput 2", newInput); // ['5', '*']
}

I understand why newInput 1 logs an array with two items, although the second item gets pushed into the array after that log. What I don’t understand is why after setInput(newInput), input doesn’t seem to be updated at all.

In order to make sense of this, I think I’d have to build the whole thing myself. Guess there’s my weekend.

The best way to update an array in state that you got through useState is apparently by doing setState([...array, newItem]); where setState is whatever state update function you have for updating the array. You shouldn’t use push, pop, shift or unshift because those won’t tell React to re-render. This is one of the mistakes I was making.

I still don’t get how to remove elements from the array or empty the array correctly through a state method returned from useState. Hopefully I can figure that out soon. And my number clicks are also always one step ahead of updates to input for some reason, so I need to how to keep them in sync if possible.

1 Like

I’m not sure if your number clicks are a step behind. I’m still in the process of getting my head around this, but it makes a difference whether you log your state within one of your handler functions or outside of it (in the top level scope of your App.js).

I tried that challenge myself and I’m a bit amazed that I got it working so fast. There’s room for improvement though, I don’t like using that evaluate function from the mathjs library. It shouldn’t be too difficult to implement a “cleaner” solution that just stores intermediate results on every operator click.

It wasn’t so much a React challenge, more a challenge to deal with fairly complex logic and a lot of conditionals. One thing I take away from this is that creating a few little helper functions with descriptive names like isFirstClick() or lastInputType() can drastically improve readability. Also, drawing it on paper like a flow chart helped a lot.

Something I noticed at the end: There was a bug in my code that the tests didn’t complain about. I could enter a second invalid decimal point, if the decimal point was the first click after calculating a result and that result being a decimal number. I’m not sure though if the tests are supposed to check for all possible invalid inputs.

Anyway here’s a link to the sandbox:

https://codesandbox.io/s/exciting-edison-pidu8

1 Like

I learned that changes to input will only appear on the next render. So I changed my code to use newInput for the stuff I was using input itself for and I’m also now using useEffect to check for updates to input and to update storedValue when input changes.

And I got all of the tests to pass (FINALLY! Man, it feels good to finally get it). I was working at it when typing this message. Got it to mostly working before I finished the post so I changed what I was going to say midway.

I just need to get it to keep the evaluated formula on the screen with an equals sign added to it when = is clicked and the result appears on the screen.

Here’s the updated code Gist.

Any help is appreciated. Thanks.

Edit: Took out useEffect and now the formula appears above the calculation result with an equals sign appended to it when = is clicked.

One question: What’s supposed to happen when I click the backspace button until the display shows “0” again and then click another number? Is it a bug if the new number clicked replaces both currentValue and storedValue completely? This is what happens with my code right now.

I created backSpaceClicked and setBackSpaceClicked functions with useState to try to fix this but it doesn’t work correctly right now.

Code in number handler:

  const handleNumberClick = event => {
    const button = event.target;
    let newInput = [...input];
    if (currentValue === "0" && button.textContent === "0") {
      return null;
    } else if ((currentValue === "0" && button.textContent !== "0") ||
    equalsClicked || reciprocalClicked || percentageClicked || squareRootClicked) {
      setCurrentValue(button.textContent);
      newInput.length = 0;
      setInput(newInput);
      setStoredValue(newInput.join(""));

      // reset it to make sure other click handlers don't misunderstand
      setReciprocalClicked(false);
      setEqualsClicked(false);
      setPercentageClicked(false);
      setSquareRootClicked(false);
    } else if (backSpaceClicked) {
      if (currentValue === "0") {
        setCurrentValue(button.textContent);
      } else if (currentValue !== "0") {
        setCurrentValue(`${currentValue}${button.textContent}`);
      }

      newInput[newInput.length - 1] = "";
      setBackSpaceClicked(false);
    }

    if (newInput.length > 0) {
      if (operators.includes(newInput[newInput.length - 1])) {
        setCurrentValue(button.textContent);
      } else if (!isNaN(newInput[newInput.length - 1]) || newInput[newInput.length - 1] === ".") {
        setCurrentValue(`${currentValue}${button.textContent}`);
      } else if (newInput[newInput.length - 1].endsWith("^2)")) {
        setCurrentValue(button.textContent);
        newInput.length = 0;
        setInput(newInput);
        setStoredValue(newInput.join(""));
      }
    }

    newInput = [...newInput, button.textContent];
    setInput(newInput);
    setStoredValue(newInput.join(""));
  };

Code in backspace click handler:

  const handleBackSpaceClick = () => {
    setBackSpaceClicked(true);
    if (currentValue.length === 1) {
      setCurrentValue("0");
    } else if (currentValue.length > 1) {
      // set currentValue to a string with the last element cut off
      const newValue = currentValue.slice(0, currentValue.length - 1);
      setCurrentValue(newValue);
    }
  };

Thoughts?

Code in comment above was updated. Just saying.

Is the behavior I’m describing for a number clicked after a backspace click buggy or is it okay?

Well first of all congratulations for passing the challenge, now comes the fun part - adding more features & refactoring.

Very naively I’d say that clicking backspace would just remove the last item in your input array, or the last item from currentValue and storedValue.
I’ve looked at your code for so long but I still don’t get why you have all those state variables keeping track of which was the last button clicked. Why does the number button need to know if backspace was clicked before?

Again, a mystery to me why you set the last item in your input array to an empty string. I’m sure you have your reasons but I don’t look through it.

One last thing that has confused me from the start is your Keypad and Button components. You have an array of 24 buttons with their ids and classNames and so on, then App.js iterates over them and renders 24 Keypad components, which all do nothing except each rendering a Button component. It would make more sense to move that whole button array into Keypad, so App renders 1 Keypad, and Keypad renders 24 Buttons. Irrelevant for functionality of course, just a more reasonable architecture in my eyes.

That suggestion about the Keypad and Button components does make sense. But how would I actually do that?

Like this in the App component’s return statement?

      <div id="keypad">
        <Keypad
          key="index"
          className="className"
          id="id"
          name="name"
          value="value"
          clickHandler="clickHandler"
        />
      </div>

But if I move the buttons array and the logic for iterating over that array to pass props to the buttons on the keypad to the Keypad component, won’t I also need to move the click handler functions over too? And what state variables need to be passed as props? I think state should remain in the App component and just be passed to the components that need something from it.

The reason I have all those state Booleans to check whether a given button was clicked is because I need to reset currentValue when those are clicked; I’m checking for those all in the same if statement and resetting currentValue there for that reason. But yeah, I don’t really need backSpaceClicked if I just need to take out the last item from currentValue and storedValue. A number click would just be appended. Easy.

Edit: For state, maybe I should everything aside from currentValue and storedValue to the Keypad component. And I should pass those two as props to the Keypad too since the click handlers need them. Unless I can do this without having to move the click handlers to the Keypad component. I have the clickHandler property on the Keypad component that tells it what click handler is for what button, but if the click handlers stay in App.js, I may have to pass them in as props the Keypad component. How will it know which handler is for which button? I need a way to map, say, props.handleNumberClick to handleNumberClick, the actual function.

I’d keep the whole logic in your App.js, you just need a function to delegate the events, which you pass down to Keypad as prop, which then passes it down to each button.

App.js

..... [logic and state] ...

const clickHandler = event => {
  const clickedButton = event.target.id;
  switch(clickedButton) {
    case 'zero':
    case 'one': 
     ...
    case 'nine': handleNumberClick(event); break;
    case 'decimal': handleDecimalClick(event); break;
    ...
  }
}

return (
    <React.Fragment>
      <Display storedValue={storedValue} currentValue={currentValue} />
      <Keypad clickHandler={clickHandler} />
    </React.Fragment>
  );

Keypad.js:


const buttons = [{}, {}, ...];

return (
    <div id="keypad">
      {buttons.map((object, index) => (
        <Button
          key={index}
          className={object.className}
          id={object.id}
          name={object.name}
          value={object.value}
          clickHandler={props.clickHandler}
        />
      ))}
    </div>
  );

One thing that took me a long time to understand is how I could handle an event that’s triggered in a Button component with a function in my App component. But it’s actually very simple, once you’ve got some routine with it. Any state variable that’s living in App can be modified by a component further down the tree, if you just pass it a function to do so. Like, in your case, a clickHandler.