DEV Community

Cover image for React: Calling functional components as functions
Igor Bykov
Igor Bykov

Posted on

React: Calling functional components as functions

TL;DR

To be a component ≠ Return JSX
<Component />Component()


Note: This article tries to explain a somewhat advanced concept.

One of my favourite things in web development is that almost any question can lead to an unforgettable deep dive that will reveal something completely new about a very familiar thing.

That just happened to me, so now I know a tiny bit more about React & want to share it with you.

It all started with a bug which we are going to reproduce now step by step. Here is the starting point:

This app contains just 2 components App & Counter.

Let's inspect App's code:

const App = () => {
  const [total, setTotal] = useState(0);
  const incrementTotal = () => setTotal(currentTotal => currentTotal + 1);

  return (
    <div className="App">
      <div>
        <h4>Total Clicks: {total}</h4>
      </div>
      <div className="CountersContainer">
        <Counter onClick={incrementTotal} />
        <Counter onClick={incrementTotal} />
        <Counter onClick={incrementTotal} />
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Nothing interesting as for now, right? It just renders 3 Counters & keeps track and displays the sum of all counters.

Now let's add a brief description to our app:

const App = () => {
  const [total, setTotal] = useState(0);
  const incrementTotal = () => setTotal((currentTotal) => currentTotal + 1);
+ const Description = () => (
+   <p>
+     I like coding counters!
+     Sum of all counters is now {total}
+   </p>
+ );

  return (
    <div className="App">
      <div>
        <h4>Total Clicks: {total}</h4>
+       <Description />
      </div>
      <div className="CountersContainer">
        <Counter onClick={incrementTotal} />
        <Counter onClick={incrementTotal} />
        <Counter onClick={incrementTotal} />
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Works perfectly as before, but now it has a shiny new description, cool!

You might notice that I declared component Description instead of just writing JSX straight inside App's return statement.
There might be plenty of reasons for that, let's just say that I wanted to keep JSX inside App's return clean & easily readable, so, I moved all messy JSX inside Description component.

You could also notice that I declared Description inside App. It's not a standard way, but Description needs to know the current state to display total clicks.
I could refactor it and pass total as a prop, but I don't plan to ever reuse Description because I need just one for the whole app!

Now, what if we also wanted to display some additional text above the central counter? Let's try to add it:

const App = () => {
  const [total, setTotal] = useState(0);
  const incrementTotal = () => setTotal((currentTotal) => currentTotal + 1);
  const Description = () => (
    <p>
      I like coding counters!
      Sum of all counters is now {total}
    </p>
  );
+
+ const CounterWithWeekday = (props) => {
+   let today;
+   switch (new Date().getDay()) {
+     case 0:
+     case 6:
+       today = "a weekend!";
+       break;
+     case 1:
+       today = "Monday";
+       break;
+     case 2:
+       today = "Tuesday";
+       break;
+     default:
+       today = "some day close to a weekend!";
+       break;
+   }
+
+   return (
+     <div>
+       <Counter {...props} />
+       <br />
+       <span>Today is {today}</span>
+     </div>
+   );
+ };

  return (
    <div className="App">
      <div>
        <h4>Total Clicks: {total}</h4>
        <Description />
      </div>
      <div className="CountersContainer">
        <Counter onClick={incrementTotal} />
-       <Counter onClick={incrementTotal} />
+       <CounterWithWeekday onClick={incrementTotal} />
        <Counter onClick={incrementTotal} />
      </div>
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

Brilliant! Now we do have a bug! Check it out:

Note how total is incremented when you click on the central counter, but the counter itself always stays at 0.

Now, what surprised me is not the bug itself, but rather that I accidentally found out that the following works seamlessly:

  return (
    <div className="App">
      <div>
        <h4>Total Clicks: {total}</h4>
        <Description />
      </div>
      <div className="CountersContainer">
        <Counter onClick={incrementTotal} />
-       <CounterWithWeekday onClick={incrementTotal} />
+       { CounterWithWeekday({ onClick: incrementTotal }) }
        <Counter onClick={incrementTotal} />
      </div>
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

Surprised as well? Let's dive into together!

The bug

The bug happens because we create brand new CounterWithWeekday on each App update.
This happens because CounterWithWeekday is declared inside App which might be considered an anti-pattern.

In this particular case, it's easy to solve. Just move CounterWithWeekday declaration outside of the App, and the bug is gone.

You might wonder why we don't have the same problem with Description if it is also declared inside the App.
We actually do! It's just not obvious because React re-mounts the component so fast, that we can't notice and since this component has no inner state, it's not getting lost as in case of CounterWithWeekday.

But why directly calling CounterWithWeekday resolves the bug as well? Is it documented somewhere that you can just call a functional component as a plain function? What is the difference between the 2 options? Shouldn't a function return exactly the same thing disregard of the way it's invoked? 🤔

Let's go step by step.

Direct invocation

From React documentation we know that component is just a plain JS class or function that eventually returns JSX (most of the time).

However, if functional components are just functions, why wouldn't we call them directly? Why do we use <Component /> syntax instead?

It turns out to be that direct invocation was quite a hot topic for discussion in earlier versions of React. In fact, the author of the post shares a link to a Babel plug-in that (instead of creating React elements) helps in calling your components directly.

I haven't found a single mention about calling functional components directly in React docs, however, there is one technique where such a possibility is demonstrated - render props.

After some experiments, I came to quite a curious conclusion.

What is a Component at all?

Returning JSX, accepting props or rendering something to the screen has nothing to do with being a component.

The same function might act as a component & as plain function at the same time.

Being a component has much more to do with having own lifecycle & state.

Let's check how <CounterWithWeekday onClick={incrementTotal} /> from the previous example looks like in React dev tools:
Alt Text

So, it's a component that renders another component (Counter).

Now let's change it to { CounterWithWeekday({ onClick: incrementTotal }) } and check React devtools again:
Alt Text

Exactly! There's no CounterWithWeekday component. It simply doesn't exist.

The Counter component and text returned from CounterWithWeekday are now direct children of App.

Also, the bug is gone now because since CounterWithWeekday component doesn't exist, the central Counter doesn't depend on its lifecycle anymore, hence, it works exactly the same as its sibling Counters.

Here are a couple of quick answers to the questions I've been struggling with. Hope it'll help someone.

Why CounterWithWeekday component is not displayed in React dev tools anymore?

The reason is that it's not a component anymore, it's just a function call.

When you do something like this:

const HelloWorld = () => {
  const text = () => 'Hello, World';

  return (
    <h2>{text()}</h2>
  );
}
Enter fullscreen mode Exit fullscreen mode

it's clear that the variable text is not a component.
If it would return JSX, it wouldn't be a component.
If it would accept a single argument called props, it wouldn't be a component either.

A function that could be used as a component is not necessarily will be used as a component. So, to be a component, it needs to be used as <Text /> instead.

Same with CounterWithWeekday.

By the way, components can return plain strings.

Why Counter doesn't lose state now?

To answer that, let's answer why Counter's state was reset first.

Here is what happens step by step:

  1. CounterWithWeekday is declared inside the App & is used as a component.
  2. It is initially rendered.
  3. With each App update, a new CounterWithWeekday is created.
  4. CounterWithWeekday is a brand new function on each App update, hence, React can't figure out that it's the same component.
  5. React clears CounterWithWeekday's previous output (including its children) and mounts new CounterWithWeekday's output on each App update. So, unlike other components, CounterWithWeekday is never updated, but always mounted from scratch.
  6. Since Counter is re-created on each App update, its state after each parent update will always be 0.

So, when we call CounterWithWeekday as a function, it is also re-declared on each App update, however, it doesn't matter anymore. Let's check hello world example once again to see why:

const HelloWorld = () => {
  const text = () => 'Hello, World';

  return (
    <h2>{text()}</h2>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this case, it wouldn't make sense for React to expect the text reference to be the same when HelloWorld is updated, right?

In fact, React cannot even check what text reference is. It doesn't know that text exists at all. React literally wouldn't notice the difference if we would just inline text like this:

const HelloWorld = () => {
- const text = () => 'Hello, World';
-
  return (
-   <h2>{text()}</h2>
+   <h2>Hello, World</h2>
  );
}
Enter fullscreen mode Exit fullscreen mode

So, by using <Component /> we make the component visible to React. However, since text in our example is just called directly, React will never know about its existence.
In this case, React just compares JSX (or text in this case). Until the content returned by text is the same, nothing gets re-rendered.

That exactly what happened to CounterWithWeekday. If we don't use it like <CounterWithWeekday />, it is never exposed to React.

This way, React will just compare the output of the function, but not the function itself (as it would, in the case if we use it as a component).
Since CounterWithWeekday's output is ok nothing gets re-mounted.

Conclusion

  • A function that returns JSX might not be a component, depending on how it is used.

  • To be a component function returning JSX should be used as <Component /> and not as Component().

  • When a functional component is used as <Component /> it will have a lifecycle and can have a state.

  • When a function is called directly as Component() it will just run and (probably) return something. No lifecycle, no hooks, none of the React magic. It's very similar to assigning some JSX to a variable, but with more flexibility (you can use if statements, switch, throw, etc.).

  • Using state in a non-component is dangerous.

  • Using functions that return JSX without being a component might be officially considered to be an anti-pattern in the future. There are edge cases (like render props), but generally, you almost always want to refactor those functions to be components because it's the recommended way.

  • If you have to declare a function that returns JSX inside a functional component (for instance, due to tightly coupled logic), directly calling it as {component()} could be a better choice than using it as <Component />.

  • Converting simple <Component /> into {Component()} might be very handy for debugging purposes.

Top comments (15)

Collapse
 
wallacesf profile image
Wallace Ferreira

Another alternative i found is the following:

// MyComponentWrapper.jsx
import React from 'react';

export const MyComponentWrapper = (props) => {
  const myComponent =
   React.useMemo(() => getMyComponent(props.type), [props.type])

 return React.createElement(myComponent, props);
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
crushstar profile image
Crush-star • Edited

Why using useState in a function is valid.
eg.

  const [state, setstate] = useState(0);

  const fun = () => {
    const [a, seta] = useState(0);
    useEffect(() => {
      seta(a + 1);
    }, [state]);
    return <div>{a}</div>;
  };

  return (
    {fun()}
  )
Enter fullscreen mode Exit fullscreen mode
Collapse
 
crushstar profile image
Crush-star

When the state changes and re-renders, the aa function is indeed the latest value

Collapse
 
manimestro profile image
Manikanta prasad • Edited

usestate hook are used by react to memorize something .. in order to work that hook should be called by react itself ..not you .. so when react calls it.. it will get an object with the info of current component and react will go find where the shelf is and get the memorized value

Collapse
 
henryyangs profile image
SIYUAN YANG

AMAZING! I met exactly the same bug yesterday, and fixed it by accident changing <Component /> to {Component()}, then found this article this morning. Thanks for your article and help me to understand.

Collapse
 
igor_bykov profile image
Igor Bykov

Glad it helped 👍

Collapse
 
_madhurgarg profile image
Madhur Garg • Edited

What if the text component contains it's own state as well? As you said in case of function call react doesn't know about the existence of text function? What will happen to the state?

const HelloWorld = () => {
const text = () => {
const [value, setValue] = useState('hello')
return 'Hello, World';
}

return (
<h2>{text()}</h2>
);
}

Collapse
 
igor_bykov profile image
Igor Bykov

Certainly the state will not work in any meaningful way

Collapse
 
codecopier profile image
codeCopier • Edited

Deep and eye-opening article! Thanks.
P.S. This article indirectly helped me figure out my problem with HOC: I needed to wrapped multiple components inside a HOC and render the result. and all of these are inside another component's return.

After reading this, I realized HOC is a function that get another function declaration as argument. So I needed to do something like this to get the results:

...
 return (
        ...
        {
            myHOC(() => {return (
                <>
                <MyComp1 />
                <MyComp2 />
                <MyComp3 />
                </>
            )})()
        }
      ...
    )
Enter fullscreen mode Exit fullscreen mode
Collapse
 
krankj profile image
Sudarshan K J • Edited

Nice article! Thanks for the deep dive.

Collapse
 
heitian_boyi profile image
vpokrityuk

Amazing article. Thanks

Collapse
 
shivraj97 profile image
Shivraj97

One of the best article I have read on React.

Collapse
 
igor_bykov profile image
Igor Bykov

Hey, so glad you liked it :)

Collapse
 
theashishmaurya profile image
Ashish maurya

I came accross similar bug but after reading your articles, i totally understood why its happening . GOOD WORK BUDDY.

Collapse
 
igor_bykov profile image
Igor Bykov

Thank you :)