Passing Functions to React Components... Simple, right?

12th Sep 2021

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:

  • Passing a function to be used by a child component
  • Invoking a function to pass a value to a child component
  • Passing a function to an event handler with an implicit event
  • Passing a function to an event handler with an explicit event
  • Passing a function to an event handler with no event

A. Passing a function to be used by a child component

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";
2
3export default function App() {
4 const [, setState] = useState(0);
5 // we can setState to trigger a re-render
6
7 const coolFunc = () => console.log("running");
8
9 return (
10 <div>
11 <CoolComponent functionIwant={coolFunc()} />
12
13 <button onClick={() => setState((s) => s + 1)}>'re-render'</button>
14 </div>
15 );
16}
17
18function 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")} />;
2
3// === JSX compiled to React API Calls
4
5React.createElement(CoolComponent, {
6 functionIwant: console.log("yeet"),
7 // functionIwant: () => console.log('yeet') <- the correct way
8});
9
10// === React.createElement call generates Object
11
12const resultOfCreateElementCall = {
13 type: "h1",
14 props: {
15 children: "COOL",
16 functionIwant: undefined, // returned value of functionIwant
17 },
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";
2
3export default function App() {
4 const [num, setNum] = useState(0); //setup some state
5
6 const handleClick = () => setNum((s) => s + 1);
7
8 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... */}
14
15 {/*
16 GOOD */}
17 <button onClick={handleClick}>Click</button>
18 {/* Passing a function, not a function call */}
19
20 {num}
21 </div>
22 );
23}
CodeSandbox

JSX

1<button
2 badOnClick={setNum((s) => s + 1)}
3 goodOnClick={() => setNum((s) => s + 1)}
4>
5 Click
6</button>;
7
8// === JSX compiled to React API Calls
9
10React.createElement(
11 "button",
12 {
13 badOnClick: setNum((s) => s + 1), // Executed every render
14 // Being called every render, setting state, which triggers a re-render...
15
16 goodOnClick: () => setNum((s) => s + 1), // Not Executed every render
17 },
18 "Click"
19);
20
21// === React.createElement call generates Object
22
23const resultOfCreateElementCall = {
24 type: "button",
25 props: {
26 children: "Click",
27 badOnClick: undefined, // returned value
28 goodOnClick: () => setNum((s) => s + 1), // the function we want
29 },
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.


B. Invoking a function to pass a value to a child component

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";
2
3export default function App() {
4 const [show, setShow] = useState(false);
5
6 const changeState = () => setShow((s) => !s);
7 //A. Passing a function to be used by a child component
8
9 const coolFunc = () => (show ? "open" : "close");
10 //B. Invoking a function to pass a value to a child component
11
12 return <CoolComponent resultIwant={coolFunc()} functionIwant={changeState} />;
13}
14
15function 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} />;
2
3// === JSX compiled to React API Calls
4
5React.createElement(
6 CoolComponent,
7 {
8 resultIwant: show ? "open" : "close",
9 functionIwant: () => setShow((s) => !s),
10 },
11 "c l i c k"
12);
13
14// === React.createElement call generates Object
15
16const resultOfCreateElementCall = {
17 type: CoolComponent,
18 props: {
19 children: "c l i c k",
20 resultIwant: "open", // or "close" <- VALUE we want
21 functionIwant: () => setShow((s) => !s), // <- FUNCTION we want
22 },
23};
CodeSandbox


Events

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);