Search code examples
javascriptreactjsasynchronousreact-hookscomponents

Calling two functions onClick in React, functions work independently but not when called simultaneously


I have a nav with buttons, each representing a category. When a button is clicked, the setActive function which is passed to the nav component as a prop from a parent file triggers a different category component to be rendered. This part of the code works as expected.

In the nav component, I also want the buttons to display an <svg> when clicked, instead of the default image for that button. I'm using the useState hook to setActiveButton when a button is clicked. I then check if activeButton === name of that button, and if so, conditionally render the <svg>.

If I pass either one or the other function to the onClick, the code works, but together, the local setActiveButton function keeps re-setting state to "null" on the initial click of a button, and only displays the <svg> after two clicks. Here's the code in the nav component:

import { useState } from "react";

const DisciplineNav = ({ setActive }) => {

  const [activeButton, setActiveButton] = useState(null);

  const handleButtonClick = (button) => {
    setActiveButton(button);
    console.log(button);
    console.log(activeButton);
  };

  const svgElement = (
    <svg
      ...
    </svg>
  );

  return (
    <nav>
      <button
        onClick={() => {
          handleButtonClick("Button1");
          setActive("Button1");
        }}
      >
        {activeButton === "Button1" ? (
          svgElement
        ) : (
          <img
            src="https://cdn..."
          />
        )}
        <br />
        <span>Button1</span>
      </button>
      <button
        onClick={() => {
          handleButtonClick("Button2");
          setActive("Button2");
        }}
      >
        {activeButton === "Button2" ? (
          svgElement
        ) : (
          <img
            src="https://cdn..."
          />
        )}
        <br />
        <span>Button2</span>
      </button>
    </nav>
  );
};

export default DisciplineNav;

Here's the parent component:


import { useState, useEffect } from "react";
import React from "react";
import env from "react-dotenv";
import axios from "axios";
import Button1 from "../../Components/ParentComponent/Button1.js"
import Button2 from "../../Components/ParentComponent/Button2.js"
import DisciplineNav from "../../Components/Discipline-Nav/DisciplineNav.js";

export const ParentComponent = () => {
  const [button1Data, setButton1Data] = useState([]);
  const [button2Data, setButton2Data] = useState([]);
  const [active, setActive] = useState("button1");
  
  useEffect(() => {
    axios
      .get(
        `...=json&key=${env.API_KEY}`
      )
      .then((response) => {
        console.log(response.data);
        setButton1Data(response.data.values);
      });
  }, []);

  useEffect(() => {
    axios
      .get(
        `...=json&key=${env.API_KEY}`
      )
      .then((response) => {
        console.log(response.data);
        setButton2Data(response.data.values);  
      });
  }, []);

  const ParentComponent = () => (
    <div>
      <div className={cls["title"]}>ParentComponent</div>
      <DisciplineNav setActive={setActive}/>
      <div>
        {active === "Button1" && <Button1 button1Data={button1Data}></Button1>}
        {active === "Button2" && <Button2 button2Data={button2Data}></Button2>}
      </div>
    </div>
  );

  return (
    <>
      <ParentComponent />
    </>
  )
}

export default ParentComponent;

When I console log the button, it's always the name of the button I just clicked, as expected. But when I console log activeButton, it logs the previously clicked button on first click, null on second click, and finally logs the button being clicked on the third click. Though for some reason on second click when activeButton is "null" the svg is rendered, which I don't understand since the conditional render should only show the svg when activeButton === the button that was just clicked. I believe the issue may have to do with how react asynchronously renders state, but I can't pin down how to solve this.

Any help would be massively appreciated!


Solution

  • Your code above defines ParentComponent inside a component (also named ParentComponent) and then renders <ParentComponent>.

    A component is a JavaScript function. When a component "renders", you are simply running that JavaScript function.

    You have defined a function (let's call it B()) inside of a function (A()). Every single time that A() is run, it will recreate B(). It is not the same B() as in the previous run.

    In React terms: you have defined a component <B> inside of the code of component <A>. Each time that <A> renders, you get a new component called <B>.

    Do not define components inside of components. 99.47% of the time you will not be happy with the results.