What makes a React function component a component as opposed to only a function? How a function is instantiated or invoked determines this.
It would be easy to assume React function components are just functions. However, whether a function behaves as a component depends on how it whether it is invoked as a function or instantiated as a component, not on how it is defined. The act of instantiating a component through a
React.createElement() call is what makes it one. In JSX, remember that
<Child /> is a call to
React.createElement(Child, , ). In our example below, invoking a child component with function syntax means we’re treating it as a plain function that returns a
ReactElement rather than as a component.
// Given this component:;
// 1. We can instantiate the child component with JSX syntax:;
// 2. We can invoke the child component with function syntax:;
What does it mean to behave as a component? It means there’s a component instance as a node in React’s component tree. Let’s first look at a component graph with these simple examples:
// 1. Instantiated as a component:Consumer|Child // <Child /> creates a component instance in the React component tree|Something|...
// 2. Invoked as a function:Consumer|| // Child() is as though its implementation were inlined into the consumer|Something|...
Of course, in the above examples, the capitalization of the
Child function should clearly indicate to an author it’s meant to be instantiated as a component rather than invoked as a function. However, the further we get from the original component definition, the less obvious this is, and the potential for error increases.
Let’s explore what this means in a real-world example:
// Some dummy data:;
// 1. The following consumer is correct:;;
// 2. The following consumer is incorrect:;
Although I labeled the above consumer examples correct and incorrect, that’s not the whole story. Whether the incorrect consumer results in incorrect behavior depends on the implementation of the
ValueItem component and
List, which are not shown. However, when we’re writing the consumer, we should not assume the implementation details of these, as they could change in the future and break our implementation. Specifically, there are two things we can’t assume:
- Whether the
ValueItemcomponent containts lifecycle hooks — or whether it will contain them at some point in the future.
- Whether the
Listcomponent instantiates the
React.createElement()or simply invokes it as a function.
In this example, the
List component we used above doesn’t invoke the
renderItem function as a component when we look at its implementation:
ValueItem component does rely on the React lifecycle through hooks.
Looking at our incorrect consumer example, instead of having one hook per
ValueItem instance, it has n hooks within a single
List component instance. This is because there isn’t a
ValueItem instance if
ValueItem isn’t instantiated as a component.
// 1. Instantiated as a component:List| (n)ValueItem -> useValue() // n component instances, each with one hook invocation|...
// 2. Invoked as a function:List| (n)| -> useValue() // n hook invocations within one component instance|...
What this means in a real project is, if we load our list of items incrementally or otherwise change the order of items within the list, we will have changed the numer or order of hook invocations. React relies on the order in which hooks are called during the render phase, so this is a serious issue.
The key point here isn’t to diligently check for this class of error along the entire code path each time we make an edit, but to avoid making assumptions about how functions will be outside the immediate chunk of implementation we’re working on.
Wherever we use a function component, as indicated through its capitalization, we need to ensure it will be instantiated as a component in order to prevent future bugs, regardless of whether it currently needs to be. As in the above example, this could be along the lines of wrapping a function component in an anonymous function to ensure the inner component is correctly instantiated when we pass it down to a different piece of code, as opposed to passing it directly.