This question concerns the definition of callback functions inside functional components, with the callback functions being passed down as props to child functional components. I am aware that when defining functions inside functional components and those functions are being passed on as props to child components we should use useCallback. In this example I am deliberately not using it.
I have a dummy React app as below:
App.jsx
import { useState, useCallback } from 'react';
import './App.css';
import TestComponent from './components/TestComponent';
import TestComponent2 from './components/TestComponent2';
function App() {
const [message, setMessage] = useState("");
console.log('Defining myArrowfunc');
const myArrowFunc = () => console.log('Hello!');
console.log('myArrowfunc defined');
return (
<>
<TestComponent message={message} myArrowFunc={myArrowFunc} />
<TestComponent2 myArrowFunc={myArrowFunc} />
<input
type="text"
value={message}
onChange={e => setMessage(e.target.value)}
/>
<button
type="button"
onClick={() => {
console.log('Setting message');
setMessage("x");
}}>Set to x</button>
</>
);
}
export default App;
TestComponent.jsx
import { useEffect } from 'react';
function TestComponent(props) {
useEffect(() => {
console.log(`TestComponent.useEffect, props.message = ${props.message}`);
}, [props]);
return <h2>TestComponent - {props.message}</h2>;
}
export default TestComponent;
TestComponent2.jsx
import { useEffect } from 'react';
function TestComponent2(props) {
useEffect(() => {
console.log(`TestComponent2.useEffect`);
}, [props.myArrowFunc]);
return <h2>TestComponent2 - Placeholder</h2>;
}
export default TestComponent2;
Starting the app, the page loads and we are here:
In the console window everything looks like expected. The console.log statements Defining myArrowFunc and myArrowFunc defined ran, and we can see the console.log statements from the useEffect hook within TestComponent and TestComponent2 which ran after they were rendered.
Now we click on the button Set to x, which invokes the callback function attached to the onClick event of the button. This callback calls setMessage("x") which updates the state of the App component and consequently triggers a re-render of the component.
In the console window we can see that the App functional component ran (e.g. from the console.log("Defining myArrowFunc)) and one can also see that the useEffect hooks of the child components ran after they were re-rendered.
Now, that TestComponent re-rendered is of course understandable. message state is a prop on TestComponent which would cause a re-render of TestComponent. Also, in the dependency array of TestComponent is specified props (not props.message), but props is a new object on every render (right?) and hence the reference comparison tells us props has changed and so useEffect runs.
That the useEffect hook of TestComponent2 ran can be understood (right?) from the fact that myArrowFunc is re-defined on each render of App, and so the prop passed to TestComponent2 has in fact changed (reference comparison).
Here comes the part where I become confused: the message state in App now holds "x", and so additional clicks to the button will not change the state, and so should not trigger a re-render of App (and any child components dependant). When I click the button the following output happens:
The statements console.log('Defining myArrowfunc'); and console.log('myArrowfunc defined'); ran. What does this mean? Did the component re-render? If the App function ran, then it should have defined a new myArrowFunc (right?), and since TestComponent2 takes that as a prop, it should have re-rendered and run its useEffect hook?
What's interesting as well is that when I again click the button, it does not look like App ran, the output becomes only "Setting message".
Would very much appreciate an outline / explanation of what exactly is going on here.



