Get eBook BundleVolume I index
Components Before Abstractions

Returning One UI Tree

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

There's one pretty normal JavaScript rule behind this entire lesson: a function call returns one value.

React components follow the same rule. Nothing special happened to JavaScript just because JSX entered the file.

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

So what did ChapterPage() return here?

One value.

That value describes a main, and inside that main we've got an h1 and a p. So yes, the UI can contain 2 elements, 20 elements, or 200 elements, but the function still returns one JavaScript value describing that whole nested React tree.

This sounds almost too obvious, but it clears up quite a few JSX errors later.

One function call, one return value

Let's remove all the nesting for a second.

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

React calls ChapterPage(), JavaScript reaches return, and the caller gets that React element value.

Normal function behavior.

And normal JavaScript rules still apply after return too:

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

That console.log() is never going to run because the function already returned.

So if you need to calculate something before creating the UI, do it before the return.

jsx
function ChapterPage() {
  const lessonCount = 6;

  return <p>{lessonCount} lessons</p>;
}

React calls the function, lessonCount gets created, and then the function returns a React node using that value.

Pretty normal JavaScript so far. React hasn't changed any function rules here.

Why can't I return two JSX elements next to each other?

Now let's try this:

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

And your compiler complains.

Why?

Because after return, JavaScript needs one expression. You've put two JSX expressions next to each other and there's nothing grouping them into one returned value.

If these two elements naturally belong inside a header, then just use a header.

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

Now the function returns one header element value, and that value contains two children.

You can also use a fragment if you genuinely don't want another DOM element:

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

A fragment groups those children for React without creating an extra element in the browser DOM.

But don't automatically throw fragments around everything just because JSX allows it. If the content really belongs inside a header, section, nav, ul, or whatever HTML element describes it properly, use that element.

The rule is simply this: your component returns one JavaScript value. That value can describe many descendants.

For example:

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

App() returned one React element, <ChapterPage />. React then sees that this is a component, calls ChapterPage(), gets its result, and continues from there.

JSX nesting creates the parent-child structure

Indentation is mostly there so humans don't suffer while reading JSX.

The actual nesting comes from the tags.

Take this:

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 the list. Then the ul contains the two li elements.

That parent-child relationship comes from how those JSX elements are nested, not from however many spaces you typed before them.

For example:

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

The second li is now outside the ul.

You can indent it however you want and make the code look convincing, but the tags say otherwise.

And that markup is invalid HTML because a list item needs to belong to an appropriate list container.

JSX can tell you when your tags aren't closed properly. It cannot guarantee that every HTML structure you write is semantically correct.

That's still your job.

React still follows HTML rules

Since React DOM eventually creates normal browser elements, normal HTML rules still apply.

This, for example, is bad markup:

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

A div cannot sit inside a p like that.

Use markup that actually allows the content you're putting inside it:

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

React doesn't give invalid HTML some special permission to become valid.

Browsers can correct invalid markup while parsing or constructing DOM structures, and those corrections can become especially annoying when server-rendered HTML gets hydrated on the client.

Same goes for lists:

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

And tables have their own allowed structure. Interactive elements have rules too. You generally don't want one interactive control nested inside another interactive control.

So yes, JSX looks different from writing HTML files directly, but once you return lowercase host elements such as <p>, <button>, <ul>, and <table>, you're still dealing with HTML.

Components don't create wrapper elements by themselves

Now let's split the UI into components.

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

Let's say ChapterHeader returns a header, and LessonList returns a section.

Then the browser DOM may look something like:

text
main
  header
  section

There isn't some automatic <ChapterHeader> DOM element around the header, and there isn't a <LessonList> DOM element around the section.

Those names belong to your React component tree.

React DevTools can show something closer to:

text
ChapterPage
  ChapterHeader
  LessonList

But open the browser Elements panel and you'll see the actual host elements those components returned.

So you can split code into many components without automatically adding many DOM wrappers. What appears in the browser depends on what those components eventually return.

A component can return plain text too

Components don't always have to return JSX elements.

This is valid:

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

And then:

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

React puts that string into the surrounding output as text.

Numbers work too:

jsx
function LessonCount() {
  return 6;
}

You don't need to add a span just because you're worried every component must return an element.

jsx
function LessonCount() {
  return <span>6</span>;
}

Use the span if you actually need that DOM element for styling, semantics, events, or some other reason. Otherwise, returning the number directly is perfectly fine.

React can render strings, numbers, React elements, arrays containing renderable values, and empty results such as null.

The surrounding component decides where that returned value ends up.

Returning null means "render nothing here"

Sometimes a component should produce no DOM output for the current props.

For example:

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

  return <span>Draft</span>;
}

If published is true, React still calls DraftBadge().

The component runs normally, checks the prop, and returns null.

React then has no host content to create for that result.

So don't read return null as "React skipped the component". The function absolutely ran. Its result for that render just happened to be empty.

And yes, a component returning null can still use Hooks.

Maybe it has state. Maybe it reads context. Maybe it runs an Effect. Returning no DOM content doesn't somehow remove the component from React's component tree.

Use null when showing nothing is an intentional UI result.

Forgotten returns can look suspiciously normal

Now we get to one that wastes more time than it should.

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

What happens when complete is false?

The function reaches the end.

And in JavaScript, a function that reaches the end without returning anything gives you undefined.

So now your component returned undefined.

Depending on where this happens, you might just end up with no visible output there. No dramatic explosion, no giant red error covering the browser, just... nothing showing up.

That's why I prefer making an intentional empty result obvious:

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

  return <p>Complete</p>;
}

Now I can read the code and immediately see that the empty UI was deliberate.

Or maybe both cases should show something:

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

Either is fine depending on what the UI needs.

The important part is that when your page suddenly has a blank area, don't assume React must have thrown some error. Check what the component actually returned for the current props and state.

Sometimes the answer is just undefined.

Arrow functions have one easy return mistake

This arrow component returns JSX:

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

Because there's no block body, JavaScript returns that expression automatically.

Now add braces:

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

Still fine, but now you had to write return yourself.

And then comes this version:

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

Looks believable if you're scanning quickly.

But there's no return.

JavaScript evaluates the JSX expression and then throws away the result. The function reaches the end and gives back undefined.

I've seen this bug enough times that if an arrow component mysteriously renders nothing, checking for a missing return is one of the first things I'd do.

Personally I prefer function declarations for most components:

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

I find them easier to scan. But arrow components are completely valid too, and plenty of codebases use them everywhere.

Just remember what adding {} does to an arrow function body.

Don't return a browser DOM node

What about this?

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

Nope.

document.createElement("h1") gives you an actual browser DOM node.

React expects values it knows how to render through its own rendering system: React elements, text, numbers, supported collections, null, and other supported React node values.

A live DOM node created manually with document.createElement() doesn't belong in the component return value.

Return JSX:

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

React can now track that output and React DOM can create or update the actual browser element when needed.

There are cases where React code has to interact directly with DOM nodes, of course. Refs exist for exactly that sort of integration. But returning a manually created DOM node from your component isn't how you do it.

Don't modify outside data while rendering

Component code also shouldn't start modifying values owned by something else while React is rendering it.

For example:

jsx
function LessonCount({ lessons }) {
  lessons.push({ title: "Extra lesson" });

  return <p>{lessons.length}</p>;
}

That lessons array came through props, and now the component is modifying the caller's array just because React rendered.

Render the component again and another lesson gets pushed.

Render again and another one gets pushed.

Some other component reading that same array now sees those modifications too.

This gets messy very quickly.

If all you wanted was the count, just read it:

jsx
function LessonCount({ lessons }) {
  const count = lessons.length;

  return <p>{count}</p>;
}

Your component reads the current input, calculates what it needs, and returns UI from that.

Changing application data should happen through whatever update mechanism owns that data, not as a side effect of React calling a component function.

Only returned JSX becomes part of the UI

Let's finish with this component:

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

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

What actually happens when React renders it?

React calls ChapterPage(). JavaScript creates the title variable. Then the JSX expression creates the React element description for the main, which contains the heading and the LessonList component. The function returns that value.

React can then continue through <LessonList />, call that component, inspect what it returned, and eventually React DOM can create or update the browser elements required by the final host output.

Anything you created inside the function but never included in the returned result doesn't magically get rendered.

For example:

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

return <main />;

hiddenHeading contains a perfectly valid React element value.

But you never returned it, and you never included it anywhere inside the value you did return.

So React has no reason to render it.

It's just an unused JavaScript variable at that point.

So what do I actually need to remember?

A component is still a JavaScript function, and one call to that function gives React one return value.

That one value can describe a huge amount of nested UI, so "one return value" does not mean "one HTML element".

If you need multiple JSX siblings, group them using an appropriate parent element or a fragment. Make sure the HTML nesting itself is valid because JSX isn't going to save you from every bad HTML structure.

A component can return text, numbers, JSX, supported collections, or null when you intentionally want no visible result. And if you're seeing nothing when you expected something, check whether execution actually reached a return.

Especially with arrow functions.

Seriously, check the missing return first. It takes five seconds and can save you a very stupid half hour.