Don't call a React function component



The difference between React.createElement and calling a function component directly

Watch "Fix 'React Error: Rendered fewer hooks than expected'" on egghead.io

I got a great question from Taranveer Bains on my AMA asking:

I ran into an issue where if I provided a function that used hooks in its implementation and returned some JSX to the callback for Array.prototype.map. The error I received was React Error: Rendered fewer hooks than expected.

Here's a simple reproduction of that error

1import React from 'react'

2

3function Counter() {

4 const [count, setCount] = React.useState(0)

5 const increment = () => setCount(c => c + 1)

6 return <button onClick={increment}>{count}</button>

7}

8

9function App() {

10 const [items, setItems] = React.useState([])

11 const addItem = () => setItems(i => [...i, {id: i.length}])

12 return (

13 <div>

14 <button onClick={addItem}>Add Item</button>

15 <div>{items.map(Counter)}</div>

16 </div>

17 )

18}

And here's how that behaves when rendered (with an error boundary around it so we don't crash this page):

In the console, there are more details in a message like this:

1Warning: React has detected a change in the order of Hooks

2called by BadCounterList. This will lead to bugs and

3errors if not fixed. For more information, read the

4Rules of Hooks: https://fb.me/rules-of-hooks

5

6 Previous render Next render

7 ------------------------------------------------------

81. useState useState

92. undefined useState

10 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

So what's going on here? Let's dig in.

First off, I'll just tell you the solution:

1- <div>{items.map(Counter)}</div>

2+ <div>{items.map(i => <Counter key={i.id} />)}</div>

Before you start thinking it has to do with the key prop, let me just tell you it doesn't. But the key prop is important in general and you can learn about that from my other blog post: Understanding React's key prop

Here's another way to make this same kind of error happen:

1function Example() {

2 const [count, setCount] = React.useState(0)

3 let otherState

4 if (count > 0) {

5 React.useEffect(() => {

6 console.log('count', count)

7 })

8 }

9 const increment = () => setCount(c => c + 1)

10 return <button onClick={increment}>{count}</button>

11}

The point is that our Example component is calling a hook conditionally, this goes against the rules of hooks and is the reason the eslint-plugin-react-hooks package has a rules-of-hooks rule. You can read more about this limitation from the React docs, but suffice it to say, you need to make sure that the hooks are always called the same number of times for a given component.

Ok, but in our first example, we aren't calling hooks conditionally right? So why is this causing a problem for us in this case?

Well, let's rewrite our original example slightly:

1function Counter() {

2 const [count, setCount] = React.useState(0)

3 const increment = () => setCount(c => c + 1)

4 return <button onClick={increment}>{count}</button>

5}

6

7function App() {

8 const [items, setItems] = React.useState([])

9 const addItem = () => setItems(i => [...i, {id: i.length}])

10 return (

11 <div>

12 <button onClick={addItem}>Add Item</button>

13 <div>

14 {items.map(() => {

15 return Counter()

16 })}

17 </div>

18 </div>

19 )

20}

And you'll notice that we're making a function that's just calling another function so let's inline that:

1function App() {

2 const [items, setItems] = React.useState([])

3 const addItem = () => setItems(i => [...i, {id: i.length}])

4 return (

5 <div>

6 <button onClick={addItem}>Add Item</button>

7 <div>

8 {items.map(() => {

9 const [count, setCount] = React.useState(0)

10 const increment = () => setCount(c => c + 1)

11 return <button onClick={increment}>{count}</button>

12 })}

13 </div>

14 </div>

15 )

16}

Starting to look problematic? You'll notice that we haven't actually changed any behavior. This is just a refactor. But do you notice the problem now? Let me repeat what I said earlier: you need to make sure that the hooks are always called the same number of times for a given component.

Based on our refactor, we've come to realize that the "given component" for all our useState calls is not the App and Counter, but the App alone. This is due to the way we're calling our Counter function component. It's not a component at all, but a function. React doesn't know the difference between us calling a function in our JSX and inlining it. So it cannot associate anything to the Counter function, because it's not being rendered like a component.

This is why you need to use JSX (or React.createElement) when rendering components rather than simply calling the function. That way, any hooks that are used can be registered with the instance of the component that React creates.

So don't call function components. Render them.

Oh, and it's notable to mention that sometimes it will "work" to call function components. Like so:

1function Counter() {

2 const [count, setCount] = React.useState(0)

3 const increment = () => setCount(c => c + 1)

4 return <button onClick={increment}>{count}</button>

5}

6

7function App() {

8 return (

9 <div>

10 <div>Here is a counter:</div>

11 {Counter()}

12 </div>

13 )

14}

But the hooks that are in Counter will be associated with the App component instance, because there is no Counter component instance. So it will "work," but not the way you'd expect and it could behave in unexpected ways as you make changes. So just render it normally.

Good luck!

You can play around with this in codesandbox:

Edit Don't call function components