What is a react component, anyways?
I used to teach a class on React. There’s no better way to start a hands-on course than “Let’s write a simple component!”. But time after time I hear — “Vladimir, what’s a component, anyways?”. Bah, what a question! It’s a react thingie. React apps are made of components. Why do you care at all? When you grow up you’ll see. But at some point in life you just have to find the definitive answer to this question, and for me this time is now.
Here’s what we know so far. Card
here is most certainly a component:
const Card = (props) => <div className="Card" {...props} />;const Page = () => { return <Card>hello</Card>;};
However, I have a strong sense that renderCard
is not:
const renderCard = (props) => <div className="Card" {...props} />;const Page = () => { return renderCard({ children: 'hello' });};
But why is that? React docs on components are not very helpful — first they say a component is a piece of UI, then that “components are like functions that return JSX”, yet neither explains why renderCard
is not a component. If we can solve this paradox, we can also find out what a react component actually is. We’ll examine 4 versions: is React component a…
- piece of UI?
- software decomposition unit?
- thing that implements some interface?
- unit of update?
And, surprise, it’s a bit of all four, really. Let’s dive in!
Before we begin: “React component is a web component” is so false it does not deserve a section in my article. React components do not implement anything from the Web Components spec.
Piece of UI
The most intuitive answer for anyone with front-end experience (both developers and designers) is that React component is a reusable piece of UI. You look at figma designs, spot recurring fragments, and these are your React components:
It’s tempting to say that React components implement the concept of UI components. It’s a good first attempt at definition. Still, it leaves some questions unanswered. Our renderCard
function is a reusable piece of UI, so it must be a component. This feels wrong. Next, React allows for components that don’t actually own any UI — most higher-order components and some libraries work that way:
// Not a single UI element!const List = (props) => { const [items, setItems] = useState([]); const add = () => setItems([...items, items.length]); return ( <> {items.map(props.renderItem)} {props.renderAdd({ add })} </> );}
So, UI components and React components are different, even if mostly overlapping, concepts. Still, if you know your way around general UI component decomposition, you’ll mostly do fine in React. But to capture the true essence of React components, we must dig further.
The architecture answer
As an architecture cosmonaut, I must now bring up component-based development. It’s just a formalization of my “react thingie” answer, defining a component as as encapsulated set of related functions and data.
This is not useless — in a react app, components are similar to classes in object-oriented programming and functions in functional / procedural programming. Your codebase would benefit from applying design principles developed for other techniques, like SOLID — you needn’t throw all the previous best practices away and reinvent them as “react insights”. Component decomposition is your primary tool for managingcoupling / god object issues.
Still, as an overly abstract definition, this includes a lot of things that are not react components. Your average React app consists of multiple layers — React view, state manager, API wrapper — and only one of these uses React components. Hooks fit the definition by encapsulating data & functions, yet they’re most certainly not components. renderCard
is a nuisance. Next one!
API contract interface
For the most boring take, let’s see what React itself considers a component, by looking at @types/react and the react-dom implementation. Actually, many things work:
- A function component that accepts props object and returns JSX:
(props: P) => ReactElement | null
- An object returned from
React.memo
,forwardRef
,Context
and some other builtins. They are called exotic components and trigger special renderer behavior. - A class that
extends React.Component
. Fun fact: since JS classes are functions under the hood, React checksprototype.isReactClass
(see react-dom) defined onComponent
to distinguish classes from functions. This means that technically you can implement a class component without extendingComponent
, which is probably a horrible idea. - Module pattern components — functions returning component-like objects, as in
Card = () => ({ render: () => <div /> })
. Their deprecation notice was probably the first time anyone learnt these existed. - Maybe a string, as in
<div /> -> jsx('div')
. According to react types it’s anElementType
, not aComponentType
, but I’ve also heard them referred to as “host components”. I frankly don’t care if we call these components, because there’s not much choice about using them anyways.
This peek inside the implementation answers at least one of our questions: higher-order component is a misnomer. If a higher-order function is a function that operates on other functions, HOC should be a component that accepts other components as props or returns a component. But the HOC itself is not a component, because it’s a function that does not return vDOM. Component factory would be a more fitting term. What’s done is done, though.
But what about Card
vs renderCard
? Both are functions that accept an object argument and return a react element, so both should be components. We could duct-tape our definition to force every component to be used properly — as a JSX <tag>
, or directly passed to a JSX runtime (jsx(s) / createElement
). But this means that nothing can intrinsically be a component. Try a thought experiment: if I serve soup in a hat, the hat does not stop being a hat, it’s just me acting weird — why then would misusing a react component prevent it from being a component?
If the “software component” answer was too abstract, this one is too concrete. We have a list of requirements any React component must satisfy, but, as seen in renderCard
, checking these boxes does not automatically make you a component. Moreover, this definition is very unstable — a change to react core (say, supporting Vue-like single-file components) can easily invalidate it. Let’s try to find some deeper meaning in react components.
Unit of update
This brings us to the answer I find most insightful: react component is a unit of update in a react app. Only components can schedule an update by calling useState / useReducer
update handle, doing this.setState
, or subscribing to context changes. And updating state always re-renders (calls the render function and generates virtual DOM for) whole components. Whether a component uses this ability is irrelevant, only the fact that it can matters.
For example, this component…
const Toggler = () => { const [state, setState] = useState(false); console.log('render'); return <button onClick={() => setState(!state)}>{String(state)}</button>;};
… can schedule an update when clicked, and this update runs every single line of Toggler
— calls useState
, logs to console, and creates vDOM for button
.
We can connect this definition with our “piece of UI” take. Not all UI is elements — sometimes, the crucial part is behavior. Behavior can be viewed as mapping actions to updates, and that’s exactly what a component with no markup does.
This definition relies on the deeper architecture of React and I don’t think it’s bound to change anytime soon. It has survived the introduction of hooks and concurrent features, and it can incorporate many more changes. It also sets React apart from other frameworks that apply updates on sub-component level, like Angular, svelte or solid.
Components can also opt out of an update using React.memo
, PureComponent
or shouldComponentUpdate
. You could argue that caching vDOM objects with e.g. useMemo
opts out of an update, too, but that’s not entirely the same, because you’re just providing a hint to the reconciler — all of the render function and the effects still run. More on this some other time.
This explains why hooks can only be called as a part of a function component. In functional react, hooks are the way to schedule an update. You can’t use hooks outside a component, because there’s nothing to update. Note: hooks inside other hooks are allowed because, as long as the outermost hook is inside a component, inner ones are inside that component as well.
Hooks themselves are not components, because updating useState
in a hook does not just update this particular hook, but the whole component that contains it. Also, a hook can not prevent a running update from happening.
Component vs render-function
Now let’s finally tackle Card
vs renderCard
confusion using our newfound definition. So far, neither Card
nor renderCard
is trying to update itself.
const Card = (props) => <div className="Card" {...props} />;const renderCard = (props) => <div className="Card" {...props} />;const Page = () => { return <> <Card>I am cool</Card> {renderCard({ children: 'I am a function' })} </>;};
Let’s change that by adding state into each implementation to paint it red on click:
const [active, setActive] = useState(false);return <div className="Card" style={{ color: active ? 'red' : null }} onClick={() => setActive(!active)} {...props}/>;
I know, useState
inside a regular function like renderCard
is not allowed but remember that we’re yet unsure whether renderCard
is a component. So, let’s run our code and test updates in both implementations. Aha, we see the difference! When clicked, Card
just updates itself, while calling setActive
in renderCard
leaks up into the real component, Page
, and there’s nothing we can do about it inside renderCard
. Card
is capable of independent updates, but updating renderCard
only works because it happens inside another component. That’s why Card
is a component, and renderCard
is not. Phew!
We’ve tried 4 explanations of what a react component actually is. Complete or not, each one gives us new insights into creating better components:
- React components are pieces of UI. Sensible UI decomposition is a great start for React component structure. But not all React components own some UI elements.
- React components are decomposition units of React apps. Applying SOLID or other API design principles to react components is a good practice. But not every single unit of a React app is a component.
- React components implement some interface, and React runtime abstracts the different implementations (function / class component /
memo
& co) away. But why then is{renderCard()}
not a component? - React components are a unit of update — they a) can update themselves b) always run render function completely and c) can opt out of a pending update.
I’m pretty satisfied with this last answer — it’s React-specific, but describes the intended behavior instead of implementation. It also gives a good reason to prefer smaller components: they’ll update faster, and less frequently. See my post on optimizing context for a concrete example.