Here’s a question you might encounter while interviewing for React developer roles:
“What is the difference between a component and a hook?”
The answer that the interviewer is likely looking for is along the lines of “A component is a building block for the UI, and hooks are utility functions provided by React to manage state and side-effects 😴”.
It’s a satisfactory answer if your goal is to find employment. But if you want to make an impression, the more daring answer is: “There is no difference 🔥”.
Disclaimer: Daring answers to simple questions is an unwise strategy on screening rounds.
First, What is a Component?⌗
The very technical answer is that it is a function that returns a ReactNode
, which is anything that makes sense to render on-screen (including JSX, strings, numbers, nulls, and so on).
Below is a valid React component:
const MyComponent = () => {
return "Hello World!";
};
Components can render dynamic inputs using props…
const MyComponent = ({ name, onClick }) => {
return <button onClick={onClick}>Hello {name}!button>;
};
…and implement stateful behaviors using hooks.
const MyCounter = () => {
const [count, increment] = useReducer((state) => state + 1, 0);
return <button onClick={increment}>Counter {count}button>;
};
From there, the sky is the limit!
const MyFancyCounter = () => {
const [count, increment] = useReducer((state) => state + 1, 0);
// Add fancy styles every time `count` doubles
const fancyClass = useMemo(() => {
const orderOfMagnitude = Math.floor(Math.log2(count));
let style = {};
if (orderOfMagnitude <= 0) return "";
switch (orderOfMagnitude) {
case 1:
return "fancy-1";
case 2:
return "fancy-2";
case 3:
return "fancy-3";
default:
// Beyond 255
return "fanciest";
}
}, [count]);
return (
<button onClick={increment} className={fancyClass}>
Counter {count}
button>
);
};
No matter what we do in the implementation, we can use it as a black box in another component by mixing it into the JSX.
const FancyCounterApp = () => {
return (
<div>
<h1>My Counter Apph1>
<MyFancyCounter />
div>
);
};
Second, What are Hooks?⌗
You’ve Got Mail!
No more than five emails per year, all densely packed with knowledge.
Hooks are React built-in functions to manage state and effects. You can create new hooks by wrapping the React-provided hooks in a function.
Hooks can accept any input and return any value. The only restrictions are the rules of hooks, which dictate that hooks must not be called conditionally. Critically, those rules are transitive, meaning that anything that calls a hook is a hook itself and must follow those rules until it is used in a component.
This is where we get to the crux of the story: given that Components transitively inherit all restrictions of hooks (rules of hooks), and are more restrictive in their output (can only return ReactNodes
): Components are indeed a subtype of hooks. Any component can be used as a hook!
Case in point, our most complex component from earlier can become a hook effortlessly:
// It's prefixed by `use` now, its a hook!
const useFancyCounter = () => {
const [count, increment] = useReducer((state) => state + 1, 0);
// Add fancy styles every time `count` doubles
const fancyClass = useMemo(() => {
const orderOfMagnitude = Math.floor(Math.log2(count));
let style = {};
if (orderOfMagnitude <= 0) return "";
switch (orderOfMagnitude) {
case 1:
return "fancy-1";
case 2:
return "fancy-2";
case 3:
return "fancy-3";
default:
// Beyond 255
return "fanciest";
}
}, [count]);
return (
<button onClick={increment} className={fancyClass}>
Counter {count}
button>
);
};
const FancyCounterApp = () => {
const myFancyCounter = useFancyCounter();
return (
<div>
<h1>My Counter Apph1>
{myFancyCounter}
div>
);
};
So far, this might seem like uninteresting semantics, but there is a practical side to this exercise.
Headless Components⌗
As previously noted, the distinguishing trait of a component is that it returns a ReactNode
. For API purposes, the ReactNode
is a black box. Since useFancyCounter()
still returns a ReactNode
, its internal state is opaque and not consumable.
The limitation matters because it prevents FancyCounterApp
from easily implementing features such as:
- rendering the
{count}
outside of the - extending button styling
- adding another button to increment the count by two
- performing some event once the count reaches 10
Since useFancyCounter
is a hook, instead of returning a ReactNode
, it can expose its event handlers and state, which FancyCounterApp
can leverage into a more flexible output.
// It's prefixed by `use` now, its a hook!
const useFancyCounter = () => {
const [count, increment] = useReducer((state) => state + 1, 0);
// Add fancy styles every time `count` doubles
const fancyClass = useMemo(() => {
const orderOfMagnitude = Math.floor(Math.log2(count));
let style = {};
if (orderOfMagnitude <= 0) return "";
switch (orderOfMagnitude) {
case 1:
return "fancy-1";
case 2:
return "fancy-2";
case 3:
return "fancy-3";
default:
// Beyond 255
return "fanciest";
}
}, [count]);
return { fancyClass, increment, count };
};
useFancyCounter
is now a headless component because it implements all Component-adjacent functionality (event handling, state management, styling) but allows its caller to distill it into JSX instead of returning a pre-baked ReactNode
.

The flexibility of this pattern is unparalleled, with none of the boilerplate that a composition-based solution would’ve brought.
const FancyCounterApp = () => {
const { fancyClass, increment, count } = useFancyCounter();
// performing some event once the count reaches 10
useEffect(() => {
if (count === 10) {
showFireworks();
}
}, [count]);
return (
<div>
<h1>My Counter Apph1>
{/* rendering the `{count}` outside of the `}
<h2>Count is at {count}h2>
<button
onClick={increment}
// extend button styling
className={clsx(fancyClass, "some-other-class")}
>
Increment by 1
button>
{/* adding another button to increment the count by two */}
<button
onClick={() => {
increment();
increment();
}}
>
Increment by 2
button>
div>
);
};
Unit Testing⌗
Headless components effectively decouple the view from the view model, making them testable individually.
// Testing full component
describe("FancyCounterApp", () => {
it("increments", () => {
render(<FancyCounterApp />);
const incrementBtn = screen.findByLabel("Increment by 1");
fireEvent.click(incrementBtn);
fireEvent.click(incrementBtn);
fireEvent.click(incrementBtn);
expect("Count is at 3").toBeInDocument();
});
});
// Testing component logic only
describe("useFancyCounter", () => {
it("increments", () => {
const { result } = renderHook(() => useFancyCounter());
act(() => result.current.increment());
a
=span>
Read More