Volume I index
Components Before Abstractions

Returning One UI Tree

Ishtmeet Singh @ishtms/July 14, 2026/8 min read
#react#components#jsx#rendering#html

A component call produces one JavaScript return value. That value can describe an entire nested UI tree.

jsx
function ChapterPage() {
  return (
    <main>
      <h1>Components Before Abstractions</h1>
      <p>Chapter 2</p>
    </main>
  );
}

The returned value is the outer main element. Its props contain the heading and paragraph as children, so one JavaScript value carries the nested structure.

One Function Call Returns One Value

JavaScript ends a function call when it reaches return.

jsx
function ChapterPage() {
  return <h1>Chapter 2</h1>;
}

The caller receives the element value. Code after that statement is unreachable.

jsx
function ChapterPage() {
  return <h1>Chapter 2</h1>;
  console.log("never runs");
}

React does not change JavaScript's return behavior. If a component needs to calculate a value, calculate it before the return.

jsx
function ChapterPage() {
  const lessonCount = 6;
  return <p>{lessonCount} lessons</p>;
}

The local calculation runs during the component call. The returned React node then represents the UI for that result.

Adjacent JSX Needs One Grouping Value

This source places two expressions in one return position.

jsx
return (
  <h1>Chapter 2</h1>
  <p>Six lessons</p>
);

The JSX transform cannot parse the adjacent elements as one JavaScript expression.

Use an HTML parent when the content has a document-level relationship.

jsx
return (
  <header>
    <h1>Chapter 2</h1>
    <p>Six lessons</p>
  </header>
);

The component still returns one value. That value contains two child elements.

A fragment provides a React grouping value without adding a DOM parent. Do not reach for one automatically. A section, header, list, or other semantic parent often represents the content more accurately.

A component can return a component element whose own result contains many nodes.

jsx
function App() {
  return <ChapterPage />;
}

App returns one element. React then calls ChapterPage and continues through its returned tree.

The Returned Tree Establishes Parent Relationships

Indentation helps humans read JSX, but element nesting establishes the parent and child structure.

jsx
function LessonList() {
  return (
    <section>
      <h2>Lessons</h2>
      <ul>
        <li>Component Identity</li>
        <li>Component Files</li>
      </ul>
    </section>
  );
}

The section contains the heading and list. The ul contains the two list items. React DOM creates the corresponding host relationships.

Move the closing ul and the tree changes. Whitespace indentation alone cannot preserve the old parent.

jsx
<ul>
  <li>Component Identity</li>
</ul>
<li>Component Files</li>

The second li is now outside the list and creates invalid document structure.

Use Valid HTML in the Tree

React lets components return host elements. The browser still applies HTML parsing and DOM rules.

This nesting is invalid because a paragraph cannot contain a div.

jsx
function LessonSummary() {
  return (
    <p>
      Lesson summary
      <div>Details</div>
    </p>
  );
}

Use a parent that permits both pieces.

jsx
function LessonSummary() {
  return (
    <section>
      <p>Lesson summary</p>
      <div>Details</div>
    </section>
  );
}

Invalid nesting can cause browser DOM correction and server hydration mismatches. React development builds can report selected nesting warnings, but source review should begin with correct HTML semantics.

Lists need list-item children.

jsx
<ul>
  <li>Props as Inputs</li>
</ul>

Tables need their supported table structure. Interactive controls should not be nested inside the same kind of interactive control.

React composition does not suspend those host rules.

Component Boundaries Can Be Invisible in the DOM

Break the tree into named components.

jsx
function ChapterPage() {
  return (
    <main>
      <ChapterHeader />
      <LessonList />
    </main>
  );
}

Suppose ChapterHeader returns a header and LessonList returns a section. The browser DOM contains those host elements beneath main.

text
main
  header
  section

The DOM does not contain wrapper nodes for ChapterPage, ChapterHeader, or LessonList merely because those component functions exist.

React DevTools retains the component tree.

text
ChapterPage
  ChapterHeader
  LessonList

This separates the component without adding a DOM wrapper for every function. The component's returned host content determines the actual document nodes.

Return Text When Text Is the Complete Result

A component can return a string.

jsx
function CompletionLabel() {
  return "Not started";
}

When rendered inside a paragraph, React DOM creates a text node for the result.

jsx
<p><CompletionLabel /></p>

A number can also be returned as text content.

jsx
function LessonCount() {
  return 6;
}

Do not wrap a value in a span only to satisfy an imagined requirement that components return elements. Add a host element when the document needs its semantics, style target, layout role, event behavior, or accessibility properties.

React can render strings, numbers, elements, arrays of renderable values, and empty results such as null. The surrounding tree determines where that result appears.

Return null for an Intentional Empty Result

A component can render no host content by returning null.

jsx
function DraftBadge({ published }) {
  if (published) {
    return null;
  }

  return <span>Draft</span>;
}

React still has a DraftBadge component occurrence in its tree. The component is called, reads its input, and returns null. React DOM has no host node to create for that result.

Returning null does not skip component execution. It only produces empty rendered output for that call.

Returning null is useful when the correct result for the current inputs is no UI at all.

Make Missing Returns Visible in Source

A JavaScript function with no reached return produces undefined.

jsx
function LessonStatus({ complete }) {
  if (complete) {
    return <p>Complete</p>;
  }
}

When complete is false, the function reaches its end and returns undefined. React can treat undefined as empty output in a child position, so the page can become blank without a thrown exception.

Make the empty result explicit.

jsx
function LessonStatus({ complete }) {
  if (!complete) {
    return null;
  }

  return <p>Complete</p>;
}

Or return UI for both cases.

jsx
return <p>{complete ? "Complete" : "In progress"}</p>;

The explicit source tells a reviewer whether empty output was chosen or forgotten.

Arrow Components Need a Returned Expression

An arrow function with an expression body returns that expression.

jsx
const ChapterTitle = () => <h1>Chapter 2</h1>;

Adding braces creates a block body and requires return.

jsx
const ChapterTitle = () => {
  return <h1>Chapter 2</h1>;
};

This version returns undefined because the block contains no return statement.

jsx
const ChapterTitle = () => {
  <h1>Chapter 2</h1>;
};

The JSX expression is created and discarded inside the function body. React receives undefined from the component call.

Function declarations keep component names and bodies clear. Arrow components are also valid when a project convention uses them.

Do Not Return a DOM Node

This component creates a browser element manually.

jsx
function ChapterTitle() {
  return document.createElement("h1");
}

A DOM node is not a normal React node description. React needs elements, strings, numbers, empty values, or supported collections so it can manage rendering and compare later output.

Return JSX.

jsx
function ChapterTitle() {
  return <h1>Chapter 2</h1>;
}

Do not return a DOM node created with browser APIs from a component. A component returns React values. Carefully integrated imperative code uses a separate integration point.

Keep Render Calculation Free of External Writes

The returned tree should follow current inputs without changing external data.

jsx
function LessonCount({ lessons }) {
  lessons.push({ title: "Extra lesson" });
  return <p>{lessons.length}</p>;
}

This mutates an array declared outside the component during rendering. A second render sees another added item, and another consumer sees changed data.

Calculate without mutation.

jsx
function LessonCount({ lessons }) {
  const count = lessons.length;
  return <p>{count}</p>;
}

Building the returned tree should read its inputs rather than modify them.

Trace Reachable Output

Use the following component.

jsx
function ChapterPage() {
  const title = "Components Before Abstractions";

  return (
    <main>
      <h1>{title}</h1>
      <LessonList />
    </main>
  );
}

Trace it in order.

  1. React calls ChapterPage.
  2. The local title binding is created.
  3. JSX creates one outer element value with two children.
  4. The function returns that value.
  5. React follows the LessonList component child.
  6. React DOM commits the reachable host result.

Only values reachable from the returned React node join the rendered tree. A JSX variable created but never included remains an unused JavaScript value.

jsx
const hiddenHeading = <h1>Unused</h1>;
return <main />;

Lesson Check

The return rule can now be stated precisely.

text
one component call produces one JavaScript return value
that value can describe a nested React tree
the tree must use valid host nesting
null represents intentional empty output
unreached returns and discarded JSX can produce blank output

Source Notes