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>
);
};
Nothing interesting as for now, right? It just renders 3 Counter
s & 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>
);
};
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>
);
};
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>
);
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:
So, it's a component that renders another component (Counter
).
Now let's change it to { CounterWithWeekday({ onClick: incrementTotal }) }
and check React devtools again:
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 Counter
s.
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>
);
}
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:
-
CounterWithWeekday
is declared inside theApp
& is used as a component. - It is initially rendered.
- With each
App
update, a newCounterWithWeekday
is created. -
CounterWithWeekday
is a brand new function on eachApp
update, hence, React can't figure out that it's the same component. - React clears
CounterWithWeekday
's previous output (including its children) and mounts newCounterWithWeekday
's output on eachApp
update. So, unlike other components,CounterWithWeekday
is never updated, but always mounted from scratch. - Since
Counter
is re-created on eachApp
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>
);
}
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>
);
}
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 asComponent()
.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 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 (16)
Another alternative i found is the following:
Why using useState in a function is valid.
eg.
When the state changes and re-renders, the aa function is indeed the latest value
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
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.Glad it helped 👍
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>
);
}
Certainly the state will not work in any meaningful way
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:
Nice article! Thanks for the deep dive.
Amazing article. Thanks
One of the best article I have read on React.
Hey, so glad you liked it :)
I came accross similar bug but after reading your articles, i totally understood why its happening . GOOD WORK BUDDY.
Thank you :)