React from the ground up

Components, composed

Splitting a page into functions that each own a piece of it, calling one component from another, and the one rule that keeps render predictable.

Part 3 of 12

Beginner14 min readBack to Courses

A page is a tree of functions

A component returns a piece of the page. Bigger pages are made by calling components from inside other components — <Header /> inside App — which is why the screen is thought of as a tree: App at the top, its children below it, their children below them.

The payoff is local reasoning. A component that renders a card knows about cards and nothing about the footer; a change to one is a change in one function. When a file grows a second component that is doing different work, that is the signal to give it its own function.

The rules that are actually load-bearing

A component is a function with a capital name that returns a single element, a fragment, or null. Its own name is used as a tag: function Card() { … } is rendered as <Card />. The capital letter is what tells the transform it is your function rather than a browser tag.

A component must be defined at the top level of the file, not inside another component. A component defined inside another is a new function on every render, so React treats it as a different component each time and throws its contents away — the classic confusing bug, and easy to avoid.

Keep render pure

Rendering should be a description, not an action: given the same data, the same picture. Nothing is changed on the way through — no counters incremented, no storage written, no element touched by hand. Those belong in event handlers and effects, the two places later chapters introduce.

Pure render is what lets React re-run your functions often and cheaply, and it is why a component that quietly changes something else is the hardest kind of bug to find here.

One file, several components

A file can hold several components, and usually should while they are small and related: a page, its header, and the row it repeats. The file is the unit of reading, not the unit of the page — the tree is what the browser sees, and it can be spread across as many files as the project likes.

The name is the contract

A component name is also the name a stack trace and the developer tools will show, so it is worth choosing for the reader rather than for the file. Card says what it draws; Thing and Wrapper say only that something is inside. The same name appears in the element tree, in warnings about keys, and in the props panel.

One page, three components

function Header() {
  return <h1>EasyCompilers Weekly</h1>;
}

function Card() {
  return (
    <article className="card">
      <h2>JSX is not HTML</h2>
      <p>It becomes function calls before it runs.</p>
    </article>
  );
}

function Footer() {
  return <footer>Written by the components themselves.</footer>;
}

function App() {
  return (
    <>
      <Header />
      <main>
        <Card />
        <Card />
      </main>
      <Footer />
    </>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

React calls the children, not you

When App returns <Card />, it does not call Card itself. It returns an element whose type is the function, and React walks the tree and calls Card when it reaches it. A component is therefore a description of a place in the tree, not an instruction to run right now.

The distinction matters when something fails. A stack trace that names Card means the problem is inside that function; a trace that names App means the props or the structure App built are wrong. Reading which function is on top is half the debugging.

Small enough to name

A component should have one reason to change. A Header that also fetches data and formats dates has three; a Header that renders a heading has one. The split is not about line count — a twenty-line component can be too broad if it owns two unrelated jobs.

Composition also makes the page testable by eye: render Card once in isolation and a mistake is local. When everything lives in one App, a missing border could be anywhere, and every change is a change to the whole page.

Defining a component inside another is the mistake to avoid: the inner function is recreated on every render, so React treats it as a different type and rebuilds its subtree. Two more: using a lowercase name, which becomes an unknown tag, and returning siblings without a fragment.

Try these next

  • Split the card heading and body into two components and render both from Card.
  • Render three <Card /> elements and give each a different hard-coded heading.
  • Move one component into its own file in a real project and import it; here, note where the import would go.

A page made of parts

Run it, then add a Footer component and render it under the main element. Next, try defining a component inside App and give it the same name twice — watch the console.

Live Preview
Console output will appear here...

A receipt assembled from parts

Run it, then add a `Tax` component between the total and the footer. Give it a heading, a line of text and a small style of its own, and render it once.

Live Preview
Console output will appear here...

Render the same component twice — <Card /> and again <Card /> — and you get two independent pieces of page. Nothing is shared between them, which is exactly why components stay simple.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does a component return?