Free lesson · React Deep Dive
The Function Component as a Primitive
The Function Component as a Primitive
You've written components since Module 2. You know they're functions. This lesson is about understanding what kind of function - what discipline a good component follows, what properties make components composable, and why React's component model works the way it does.
The framing for the lesson is a sentence: a component is a pure function from props (and state) to UI. Most of what makes a good component follows from taking that sentence seriously. Most of what makes a bad component follows from violating it.
I'll be specific about what "pure" means here, because it's a technical claim with real consequences. I'll cover what changes when you internalize it. And I'll talk about identity - the fact that the same component function can have many instances, each with independent state.
This is the conceptual lesson before the practical ones. Lessons 4.2-4.5 will give you the patterns; this lesson gives you the model the patterns serve.
A Component, As a Function
The simplest possible component:
function Greeting({ name }: { name: string }) {
return <p>Hello, {name}</p>
}
Read this as the function it is. It takes one argument (an object with a name property). It returns a value (a piece of JSX). It has no side effects, no time-dependent behavior, no hidden inputs. Same input → same output.
That's the ideal shape. A component is a function from its inputs to its output. The inputs are props (and, when relevant, state - which we'll cover in 4.2 and Module 5). The output is JSX, which is a description of UI.
The mental model:
flowchart LR
Inputs["Inputs<br/>(props, state)"]
Component["Component function<br/>(your code)"]
Output["Output<br/>(JSX → UI description)"]
Inputs --> Component --> Output
style Component fill:#a78bfa,stroke:#8b5cf6,color:#000
A diagram of the component as a function. Inputs flow in from the left - props passed by the parent, state managed via hooks. The component function processes those inputs. The output is JSX, a description of what UI should appear given the current inputs. The function has no other inputs, no other outputs. It doesn't read from the DOM, it doesn't write to the DOM directly, it doesn't depend on global mutable state. Same inputs in, same JSX out. The component is a function, in the strict sense.
This isn't a metaphor. It's the literal mechanical model. React calls your function. The function returns JSX. React handles the rest.
The implication: you don't have to think about a component as something that "does things" over time. You can think about it as something that describes UI given inputs. The temporal behavior - when re-renders happen, what the DOM looks like at any moment - is downstream of the function's output. The function itself is timeless: same inputs → same JSX.
This is harder than it sounds for engineers coming from class-component React or from class-based UI frameworks generally. The class model frames a component as a stateful object that responds to events, manipulates the DOM, and lives across time. The functional model says: forget all of that; the component is just a function; React handles the time.
If you've never written class components, lucky you. The functional model is the only one you need. If you have written them, the unlearning is real but worth doing. The functional model is simpler and more composable.
What "Pure" Means
I said the ideal is a "pure function." That's a specific technical claim. A pure function has two properties:
1. Deterministic. Same input, same output. Always.
2. No side effects. Calling the function doesn't change anything outside of itself. No mutating external variables, no writing to global state, no triggering events, no logging that matters to behavior, no fetching from the network.
In React-speak, a pure component:
- Returns the same JSX for the same props.
- Doesn't modify props (they're read-only - we'll cover this in 4.2).
- Doesn't manipulate the DOM directly.
- Doesn't read from or write to global mutable state during render.
- Doesn't perform side effects (network requests, timers, subscriptions) during render.
In code, the difference between pure and impure:
// Pure: same input, same output, no side effects.
function Greeting({ name }: { name: string }) {
return <p>Hello, {name}</p>
}
// Impure: reads from a global mutable variable.
let lastGreeted: string | null = null
function ImpureGreeting({ name }: { name: string }) {
lastGreeted = name // ← side effect: mutates external state
return <p>Hello, {name}</p>
}
// Also impure: depends on time (different output for same input).
function CurrentTime() {
return <p>The time is {new Date().toISOString()}</p> // ← non-deterministic
}
The first is pure. The second mutates external state every time it runs - bad. The third returns a different value every render - also bad, because two consecutive calls with the same props give different outputs.
These aren't theoretical concerns. React explicitly relies on purity. It might call your function multiple times for the same props (in development StrictMode, it does this on purpose to surface impurity bugs). It might skip calls it can prove are equivalent (with memoization, covered in Module 17). It might run renders out of order (in concurrent mode). All of these optimizations assume your function is pure. Violate the assumption and you get weird, hard-to-debug behavior.
So: pure components, by default, always. Effects and impurities go in useEffect (Module 6), where they're clearly marked and live outside the render path.
Why This Matters Mechanically
Purity has a specific, mechanical payoff that's worth making explicit.
Recall from Module 2 that React might call your component function many times - on initial mount, on state change, on parent re-render. The function is the cheap part; React calls it freely.
If the function is pure, React can call it as often as it needs without consequences. The behavior is the same. Optimizations like batching, memoization, and concurrent rendering all work transparently.
If the function isn't pure - if it mutates external state, or if it depends on external state that's changing - then calling it twice produces different results. React's optimizations break. Bugs appear at boundaries: works in one render, fails in the next, no clear pattern.
This is why React's official docs are insistent about purity. It's not a stylistic preference; it's a load-bearing assumption of the runtime.
If you're tempted to violate purity - "just this once, I'll mutate this variable during render" - don't. There's almost always a clean way to do what you want with state or effects. We'll cover the clean patterns in subsequent modules.
Single Purpose
A second discipline beyond purity: a component should do one thing.
This is hard to define precisely, because "one thing" is a judgment call. But the test is: can you describe what the component does in one sentence without using the word "and"?
Good:
Greetingdisplays a greeting message.UserCardshows a user's profile summary.LikeButtontoggles whether the current item is liked.TodoListrenders a list of todos.
Less good (because they're doing two things):
UserCardWithEditFormshows a user's profile and lets you edit it. (Two responsibilities.)HeaderAndNavigationis the page header and the navigation menu. (Should be two components.)TodoListWithFilterControlsrenders the list and the filter UI. (Composition would be cleaner.)
When a component starts to need an "and" in its description, that's the signal to split it. Two smaller components with clear responsibilities are almost always better than one larger one that conflates them.
The benefits of single-purpose components:
Reuse. A UserCard can appear in three different contexts (a list, a search result, a sidebar). A UserCardWithEditForm only appears in the one specific place that needs the form.
Testability. You can test what a Greeting does in isolation. Testing UserCardWithEditForm requires testing two things at once - and probably means a more brittle test.
Composability. Single-purpose components compose. Multi-purpose ones don't - you can't extract their parts.
Readability. Reading <UserCard user={user} /> tells you what's on the screen. Reading <UserCardWithEditForm user={user} editable={true} ... /> is murkier.
This discipline gets practical when you're writing your fifth component in a file and realize the original one is doing too much. The refactor is almost always: "extract this part into its own component." The signal is the "and" in the description.
Instances
A component is a function. But when you use a component in JSX, you create an instance of it. Different instances of the same component have independent state.
The Counter example from Module 2:
function App() {
return (
<>
<Counter />
<Counter />
<Counter />
</>
)
}
Three instances of Counter. Each one calls useState(0) independently. Each has its own count. Clicking the button on one of them doesn't affect the others.
The mental model: JSX is the recipe; instances are what gets cooked. The Counter function says "every instance of this component will have a count, a +1 button, and the logic below." Each <Counter /> in the JSX is an instance that follows that recipe but lives independently.
A consequence: state is local to an instance. Two instances of the same component can have wildly different state. You don't share state between instances unless you explicitly lift it (Module 5 covers this). The default is independence.
Another consequence: React tracks instances by position in the tree. This connects to keys (Lesson 3.5) - when React reconciles, it has to figure out which instance from the previous render maps to which instance in the new render. Position is the default; keys override the default when position isn't reliable.
When you write a component, you're writing a recipe. When you use it in JSX, you're declaring an instance. The distinction is small but matters when you start thinking about state management, lists, and component identity in subsequent modules.
Components, Not Classes
A meta-point: React deliberately doesn't have inheritance among components.
In some UI frameworks (older Angular, older Ember), you'd extend a base component class to share behavior. React doesn't work that way. There's no extends relationship between components. You can't say "this component is a subclass of that one."
Why? Because inheritance has known problems for code reuse - the diamond problem, the fragility of base classes, the difficulty of refactoring across inheritance hierarchies. React's authors made a deliberate choice to avoid those problems by making composition the primary reuse mechanism.
We'll cover composition in Lesson 4.3. The point here is: when you want to share behavior between components, you don't reach for inheritance (it doesn't exist). You reach for one of three things:
- Composition - a component renders another component, possibly passing props.
- Custom hooks - shared stateful logic extracted to a hook function (Module 12).
- Utility functions - pure helpers shared via imports.
All three work via composition or function-level reuse. None work via inheritance. The choice is intentional and it's part of what makes React composable in the first place.
What I Want You to Take From This Lesson
Six things.
One: A component is a pure function from inputs to JSX. Same inputs, same JSX, no side effects during render.
Two: Purity isn't a preference; React's optimizations rely on it. Violations produce hard-to-debug behavior.
Three: A component should do one thing. If you need "and" to describe it, split it.
Four: Side effects go in useEffect, not in render. Module 6 covers this; for now, no fetching, mutation, or external side effects during render.
Five: JSX is the recipe; instances are what gets cooked. Each <Component /> in JSX creates an instance with independent state.
Six: React has composition, not inheritance. Composition, custom hooks, utility functions - those are your reuse mechanisms.
Try It
Exercise: Warmup (10 minutes). Take three components from any tutorial or codebase. For each, write a one-sentence description of what it does. Note any that need "and."
Exercise: Core (20 minutes). Write an impure component on purpose - one that depends on
Math.random()during render. Render two copies. Notice the inconsistency. Then make it pure (move the random number into state initialized once). Notice the consistency.
Exercise: Core (20 minutes). Take the Counter from Module 2. Render five instances in a column. Click each in different orders. Verify each has independent state.
Exercise: Stretch (30 minutes). Read React docs - Keeping Components Pure. Note specifically what the docs say about StrictMode's double-invocation in development; verify by adding a
console.logto a component and observing the doubled output.