Skip to content

Instantly share code, notes, and snippets.

@Shrugsy
Last active July 31, 2021 14:01
Show Gist options
  • Save Shrugsy/f38ed8c6c330ccc3483187f02e79a3a3 to your computer and use it in GitHub Desktop.
Save Shrugsy/f38ed8c6c330ccc3483187f02e79a3a3 to your computer and use it in GitHub Desktop.
Why is my returned JSX not showing?

Why is my returned JSX not showing?

The problem

You decide you want to show a component on click, so you do this.

function Bar() {
  return <div>This is BAR</div>
}

function Foo() {
  return (
    <div>
      <h3>This is my component</h3>
      <button
        onClick={() => {
          return <Bar />; // <--
        }}
      >
        Show 'Bar'
      </button>
    </div>
  );
}

You click the button, but you don't see This is BAR anywhere. 😡 What happened?

This is happening because the 'Bar' component is not returned as part of any component's render body.

This is the fix:

function Bar() {
  return <div>This is BAR</div>
}

function Foo() {
+ const [isBarVisible, setIsBarVisible] = useState(false);
  return (
    <div>
      <h3>This is my component</h3>
      <button
        onClick={() => {
-         return <Bar />; // <--
+         setIsBarVisible(true);
        }}
      >
        Show 'Bar'
      </button>
+     {isBarVisible ? <Bar /> : null}
    </div>
  );
}

In-depth explanation

Ultimately this scenario stems from two misunderstandings:

  1. Tunnel visioning on the thought that "clicking the button should show 'Bar'"
  2. Correlating 'returned jsx' with 'rendered elements'. I.e. thinking that if jsx is returned somewhere, it should show on the DOM somewhere

It's common to see developers learning react, and jsx syntax in particular, to correlate 'returned jsx' with 'rendered elements'. However, a more accurate statement is:

Elements returned from the render body of a rendered component will be rendered on the DOM.

I.e. 'returning anywhere' isn't a good enough criteria.

Remember that with React, the only way to show different UI is by updating state and then re-rendering

-- Mark Erikson

To see why the original approach is invalid, let's first look at some valid scenarios for rendering elements.

Storing an element in a variable

// ✅ This is perfectly valid. A jsx element is being stored in a variable, 
// which can be referenced later.
const el = <div>this is an element</div>;

function Foo() {
  return <div>{el}</div>
}

Returning an element from a function

// ✅ This is perfectly valid. A jsx element is being returned from a function,
// which can be called later.
const getEl = () => <div>this is an element</div>;

function Foo() {
  return <div>{getEl()}</div>
}

Note that if abstractions are removed, both of the above are equivalent to:

function Foo() {
  return <div><div>this is an element</div></div>
}

So this meets the required criteria: the element is returned from the render body of the Foo component (with the assumption that Foo itself is rendered).

So why exactly is the original example incorrect? Looking at the original code again, we have this:

function Bar() {
  return <div>This is BAR</div>
}

function Foo() {
  return (
    <div>
      <h3>This is my component</h3>
      <button
        onClick={() => {
          return <Bar />; // <--
        }}
      >
        Show 'Bar'
      </button>
    </div>
  );
}

The button element is rendered, that is fine. And on click, the callback returns the Bar component. However, where would Bar be expected to be rendered in the DOM tree? Would it be before the button? After it? After the outer div?

When we realise that any jsx element can be treated like a variable, it's easier to follow what's going on. If we try to keep track of the returned <Bar />, it is actually returned from the onClick callback. However, a plain button element does nothing with the returned value in an onClick callback, so our <Bar /> element goes nowhere. Thus, we don't meet the criteria of returning it within the render body of the component. It never makes it past that callback.

A simplified analogy is like so:

function onClick(callback) {
  callback(); // execute the callback, but don't worry about the return value
}

const add2 = (num) => num + 2;

onClick(() => {
  return add2(3); // the 'return' here is pointless
});

The callback given to the onClick will be executed, but returning the value in the callback is pointless. The callback runs, but onClick doesn't care about what it returns, and doesn't pass the return value along or use it anywhere else.

Hence, the takeaway is that if you expect an element in a component to be rendered, it needs to be able to be traced back to the render body of a component somewhere*.

* Note that elements can be rendered directly with no components involved, but for the purpose of this discussion, we won't get into that

It is worth noting that some components intentionally do take advantage of patterns that involve returning elements in prop callbacks, but that is out of the scope of this article. You can have a look at render props if you're interested in that topic. However, a button onClick is definitely not a render prop!

The solution to the problem, as shown above, is to use component state, and conditional rendering. By using these two concepts, we can meet two important criteria for this problem:

  1. Returning elements within the returned render body of the component
  2. Describing our UI as a function of our state

Looking at the solution again:

function Bar() {
  return <div>This is BAR</div>
}

function Foo() {
+ const [isBarVisible, setIsBarVisible] = useState(false);
  return (
    <div>
      <h3>This is my component</h3>
      <button
        onClick={() => {
-         return <Bar />; // <--
+         setIsBarVisible(true);
        }}
      >
        Show 'Bar'
      </button>
+     {isBarVisible ? <Bar /> : null}
    </div>
  );
}

By adding the isBarVisible state, we can use conditional rendering to show our Bar component with the following line:

{isBarVisible ? <Bar /> : null}

This line is saying "on any given render, if the isBarVisible state is truthy, render the Bar component here, otherwise render nothing here".

We also change our button callback to simply call:

setIsBarVisible(true);

By making these changes, we stick to a core react pattern:

  • Describe what the UI should look like given some particular state (including the conditional rendering of Bar)
  • Trigger side effects after a render is done, which may include updating state (In this case, setting some state in the onClick callback)
  • Rinse and repeat

That's it! Sticking to the above pattern is the first step to working with React, not against it.

Further Reading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment