Jimmy
Jimmy

Reputation: 3880

Invoking a function from a child component in a parent component via callback

See codesandbox here

I want to use a callback in a child react component so that I can invoke a function from my child component in my parent component. However, when I handle the callback in my parent component, and try to set this child's function so that I can invoke this function later on when a button gets clicked, this function ends up getting invoked unexpectedly.

What I want to happen is that the sampleFunction to be invoked when the 'Invoke Sample Function' button is clicked, but instead, sampleFunction is invoked when the parent component is mounted (and the console is thus logged with 'foo'). How can I properly pass this callback function from the child to the parent? Thanks.

index.js:

import React, { useEffect } from "react";
import ReactDOM from "react-dom";

const Component = ({ callback }) => {
  const [toggleStatus, setToggleStatus] = React.useState(false);

  useEffect(() => {
    callback({
      toggleStatus,
      sampleFunction: () => console.log("foo")
    });
  }, [callback, toggleStatus]);

  const handleClick = () => {
    setToggleStatus(prevState => !prevState);
  };

  return (
    <>
      <button type="button" onClick={handleClick}>
        Click Me
      </button>
      <h1>{toggleStatus ? "On" : "Off"}</h1>
    </>
  );
};

const App = () => {
  const [fn, setFn] = React.useState(null);

  const handleCallback = props => {
    setFn(props.sampleFunction);
  };

  return (
    <>
      <Component callback={handleCallback} />
      <button type="button" onClick={fn}>
        Invoke Sample Function
      </button>
    </>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Upvotes: 0

Views: 534

Answers (3)

Jimmy
Jimmy

Reputation: 3880

See codesandbox here

Thanks to @StackedQ and @Anthony for the help. As Anthony mentioned, in order to avoid an endless loop, I had to add an extra condition to handleCallback:

index.js:

import React, { useEffect } from "react";
import ReactDOM from "react-dom";

const Component = ({ callback }) => {
  const [toggleStatus, setToggleStatus] = React.useState(false);

  useEffect(() => {
    callback({
      toggleStatus,
      sampleFunction: () => () => setToggleStatus(prevState => !prevState)
    });
  }, [callback, toggleStatus]);

  const handleClick = () => {
    setToggleStatus(prevState => !prevState);
  };

  return (
    <>
      <button type="button" onClick={handleClick}>
        Click Me
      </button>
      <h1>{toggleStatus ? "On" : "Off"}</h1>
    </>
  );
};

const App = () => {
  const [fn, setFn] = React.useState(null);

  const handleCallback = props => {
    if (!fn) {
      setFn(props.sampleFunction);
    }
  };

  return (
    <>
      <Component callback={handleCallback} />
      <button type="button" onClick={fn}>
        Invoke Sample Function
      </button>
    </>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Upvotes: 0

Shivang Gupta
Shivang Gupta

Reputation: 3339

You need to update your code as you are calling the callback in the initialization of your child component.

useEffect(() => {
    // remove below call
    callback({
      toggleStatus,
      sampleFunction: () => console.log("foo")
    });
  }, [callback, toggleStatus]);

Upvotes: 0

StackedQ
StackedQ

Reputation: 4139

Based on what this question covers and the usage you want, you should use useCallback (docs - more info) instead of useEffect. something like the following is kind of better implementation:

import React from "react";
import ReactDOM from "react-dom";

const Component = ({ callback }) => {
  const [toggleStatus, setToggleStatus] = React.useState(false);


  const handleClick = React.useCallback(() => {
    setToggleStatus(prevState => !prevState);
    callback({
      toggleStatus,
      sampleFunction: () => () => console.log("foo")
    })
}, [callback, toggleStatus])


  return (
    <>
      <button type="button" onClick={handleClick}>
        Click Me
      </button>
      <h1>{toggleStatus ? "On" : "Off"}</h1>
    </>
  );
};

const App = () => {
  const [fn, setFn] = React.useState(() => null);

  const handleCallback = props => {
    console.log(props)
    setFn(props.sampleFunction);
  };

  return (
    <>
      <Component callback={handleCallback} />
      <button type="button" onClick={fn}>
        Invoke Sample Function
      </button>
    </>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Upvotes: 1

Related Questions