skip to Main Content

I’ve a Parent-Child component structure. In parent, I’ve a function that calls a web service and take few seconds to complete. From parent, I’m passing this function as a prop to the child. Child component displays buttons with an onClick tied to parent’s function. How can I disable the buttons in child after click, till the parent’s callback function finishes execution, and then re-enable them?

e.g. in code below, doOperation is such a function with a setTimeout() to simulate a web service call delay:

Parent Component:

export default function App() {
  const [val, setVal] = useState(0);
  const doOperation = (qty) => {
    setTimeout(() => {
      setVal((prev) => prev + qty);
    }, 5000);
  };

  return (
    <div className="App">
      <input placeholder={val} />
      <div style={{ display: "flex" }}>
        <ChildButtons qty={1} doOperation={doOperation} />
      </div>
    </div>
  );
}

Child Component:

const ChildButtons = ({ qty, doOperation }) => {
  const [disabled, setDisabled] = useState(false);
  return (
    <div>
      <button
        onClick={() => {
          // How can I disable the button prior to call and enable after it.
          setDisabled(true);
          doOperation(qty);
          setDisabled(false);
        }}
        disabled={disabled}
      >
        {" "}
        + {qty}
      </button>
      <button
        onClick={() => {
          // How can I disable the button prior to call and enable after it.
          setDisabled(true);
          doOperation(-1 * qty);
          setDisabled(false);
        }}
        disabled={disabled}
      >
        {" "}
        - {qty}
      </button>
    </div>
  );
};

https://codesandbox.io/s/quizzical-easley-7oyeyk

Update:

Although the sample code above has only two pairs of button, there potentially could be any number of buttons. What I have is a scrollable list with two button per list item, so technically, there can be any number of buttons on the display in pairs of two.

2

Answers


  1. You have to lift the state to the parent component:

    export default function App() {
      const [val, setVal] = useState(0);
      const [loading, setLoading] = useState(false);
      const doOperation = (qty) => {
        setLoading(true);
        setTimeout(() => {
          setVal((prev) => prev + qty);
          setLoading(false);
        }, 5000);
      };
    
      return (
        <div className="App">
          <input placeholder={val} />
          <div style={{ display: "flex" }}>
            <ChildButtons loading={loading} qty={1} doOperation={doOperation} />
          </div>
        </div>
      );
    }
    

    And use it inside the child:

    const ChildButtons = ({ qty, doOperation, loading }) => {
      return (
        <div>
          <button
            onClick={() => doOperation(qty)}
            disabled={loading}
          >
            {" "}
            + {qty}
          </button>
          <button
            onClick={() => doOperation(-1 * qty)}
            disabled={loading}>
            {" "}
            - {qty}
          </button>
        </div>
      );
    };
    
    Login or Signup to reply.
  2. You have to "promisify" the callback function so that you can await it (at least that’s the simplest way – you could also do this with .then, or, more verbosely, in callback style by passing callbacks to the parent function that it will perform before and after the main function):

    Parent Component:

    const sleep = (delay) => new Promise(resolve => setTimeout(resolve, delay));
    
    export default function App() {
      const [val, setVal] = useState(0);
      const doOperation = async (qty) => {
        await sleep(5000);
        setVal((prev) => prev + qty);
      };
    
      return (
        <div className="App">
          <input placeholder={val} />
          <div style={{ display: "flex" }}>
            <ChildButtons qty={1} doOperation={doOperation} />
          </div>
        </div>
      );
    }
    

    (so the parent component hasn’t actually really changed, I’ve just converted the setTimeout calls to promises to make it easier to "wait for them" in the child component).

    Child Component:

    const ChildButtons = ({ qty, doOperation }) => {
      const [disabled, setDisabled] = useState(false);
      return (
        <div>
          <button
            onClick={async () => {
              setDisabled(true);
              await doOperation(qty);
              setDisabled(false);
            }}
            disabled={disabled}
          >
            {" "}
            + {qty}
          </button>
          <button onClick={() => doOperation(-1 * qty)}>
            {" "}
            - {qty}
          </button>
        </div>
      );
    };
    

    (It’s not clear what the second child button is supposed to do here. You can do the same as in the first button, although in the original code you don’t appear to be trying to do that, so I’ve left it as is here. Looking at your response to the other answer, if you want the second button disabled independently of the first you will of course need 2 separate state variables in the child component, one for the disabled state of each button.)

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search