While learning React, passing functions to components initially confused me.
Not understanding how to do this correctly will result in frustration and infinite loops.
There are many different uses of functions in regards to component props:
In this example, we want to pass down 'coolFunc' for CoolComponent to use via the functionIwant prop, but instead it is being invoked on every render (logging out 'running'). Within CoolComponent, if we console log the type of functionIwant, we discover that it is undefined.
1import { useState } from "react";23export default function App() {4 const [, setState] = useState(0);5 // we can setState to trigger a re-render67 const coolFunc = () => console.log("running");89 return (10 <div>11 <CoolComponent functionIwant={coolFunc()} />1213 <button onClick={() => setState((s) => s + 1)}>'re-render'</button>14 </div>15 );16}1718function CoolComponent({ functionIwant }) {19 // console.log(typeof functionIwant);20 return <h1>COOL</h1>;21}CodeSandbox
Now, why is that?
The answer is JSX.
Our CoolComponent is simply a React.createElement call under the JSX sugar.
1<CoolComponent functionIwant={console.log("yeet")} />;23// === JSX compiled to React API Calls45React.createElement(CoolComponent, {6 functionIwant: console.log("yeet"),7 // functionIwant: () => console.log('yeet') <- the correct way8});910// === React.createElement call generates Object1112const resultOfCreateElementCall = {13 type: "h1",14 props: {15 children: "COOL",16 functionIwant: undefined, // returned value of functionIwant17 },18};CodeSandbox
The less important part of this is that 'CoolComponent' is passed as the first argument, this function is run and returns more JSX that returns a 'h1' tag and children of 'COOL'.
The important part is the second argument, which is the component's props. As you can see, this is just an object with properties corresponding with the props we passed.
Function invocations cannot live on objects, they run before the object is created and the return value of the function is assigned to the property. For passing a function to be used by a child component we want to pass a function itself instead.
In our specific example, when we click the button to re-render, the createElement function is being called, which in turn is executing our console log. In the resulting object, functionIwant is undefined because functions that have no explicit return value are undefined.
This example causes a harmless console log every render. But below is a common instance of this mistake involving setting state.
1import { useState } from "react";23export default function App() {4 const [num, setNum] = useState(0); //setup some state56 const handleClick = () => setNum((s) => s + 1);78 return (9 <div>10 {/* BAD */}11 <button onClick={handleClick()}>Click</button>12 {/* This causes an infinite render loop */}13 {/* It is being called every render, setting state, which triggers a re-render... */}1415 {/*16 GOOD */}17 <button onClick={handleClick}>Click</button>18 {/* Passing a function, not a function call */}1920 {num}21 </div>22 );23}CodeSandbox
1<button2 badOnClick={setNum((s) => s + 1)}3 goodOnClick={() => setNum((s) => s + 1)}4>5 Click6</button>;78// === JSX compiled to React API Calls910React.createElement(11 "button",12 {13 badOnClick: setNum((s) => s + 1), // Executed every render14 // Being called every render, setting state, which triggers a re-render...1516 goodOnClick: () => setNum((s) => s + 1), // Not Executed every render17 },18 "Click"19);2021// === React.createElement call generates Object2223const resultOfCreateElementCall = {24 type: "button",25 props: {26 children: "Click",27 badOnClick: undefined, // returned value28 goodOnClick: () => setNum((s) => s + 1), // the function we want29 },30};CodeSandbox
We should now understand why and how functions behave in regard to component props. Now we know that anything that you pass as props is actually being used as a value on an ordinary object. We can now use this understanding to facilitate the behaviours we want.
Sometimes we may want our function to be called every render because we want to pass down the returned value of said function.
1import { useState } from "react";23export default function App() {4 const [show, setShow] = useState(false);56 const changeState = () => setShow((s) => !s);7 //A. Passing a function to be used by a child component89 const coolFunc = () => (show ? "open" : "close");10 //B. Invoking a function to pass a value to a child component1112 return <CoolComponent resultIwant={coolFunc()} functionIwant={changeState} />;13}1415function CoolComponent({ resultIwant, functionIwant }) {16 return (17 <>18 <button onClick={functionIwant}> c l i c k </button>19 <h1>{resultIwant}</h1>20 </>21 );22}CodeSandbox
In this example we utilise both behaviours. A & B.
1<CoolComponent resultIwant={coolFunc()} functionIwant={changeState} />;23// === JSX compiled to React API Calls45React.createElement(6 CoolComponent,7 {8 resultIwant: show ? "open" : "close",9 functionIwant: () => setShow((s) => !s),10 },11 "c l i c k"12);1314// === React.createElement call generates Object1516const resultOfCreateElementCall = {17 type: CoolComponent,18 props: {19 children: "c l i c k",20 resultIwant: "open", // or "close" <- VALUE we want21 functionIwant: () => setShow((s) => !s), // <- FUNCTION we want22 },23};CodeSandbox
Although Event Handlers are still props, they have a unique behaviour in which they implicitly provide an event argument to any function passed.
Or do they?
Infact, your function is simply provided as the listener argument of addEventListener
1target.addEventListener(type, listener);