DEV Community

Cover image for Declarative Conditional Wrappers in React
Khaled Elmorsy
Khaled Elmorsy

Posted on

Declarative Conditional Wrappers in React

The Problem

You're defining a Username component and, depending on the pageURL prop, you either wrap your JSX in an anchor element or you don't.

Image description

Sounds simple enough, and with a simple element like our example, it doesn't take that many extra lines.

const Username = ({username, pageURL}) => (
  <div>
    {pageURL 
      ? <a href={pageURL}>{username}</a>
      : username
    }
  </div>
)
Enter fullscreen mode Exit fullscreen mode

If our inner JSX gets too unwieldy we can try saving it as its own component or a local variable and use the ternary operator again.

While the ternary operation is declarative and clear, the idea of repeating the same inner elements on both sides of its conditional return begs for a different approach.

What we want is a way to declaratively wrap JSX elements depending on specific conditions

A Solution

When I faced this problem in my own project and set out to solve it, I imagined nested container components similar to React Router. Something that looks like this:

const Username = ({username, pageURL}) => (
  <div>
    <Optional when={pageURL}>
      <a href={pageURL}> // Conditionally rendered
        <Target>
          {username}
        </Target>
      </a>
    </Optional>
  </div>
)
Enter fullscreen mode Exit fullscreen mode

I liked this approach a lot, and it definitely felt, very much, within reach.

The Plan:

  1. Define an Target component which simply returns its children
  2. Define an Optional component, which finds a nested Target component, and depending on its when prop, either render just the Target or all its children.

The tricky part was in step 2, namely finding the nested Target.

To do it, we needed to:

  1. Find a way to search through the Optional component's children efficiently.
  2. Figure a way to identify the Target component.

Searching for the Target
To achieve step 1 we'll first note that wrapping elements have a children prop which can either be a single child element, or an array of elements.

Efficiently iterating over this element tree depends the problem. For our case, we can nominally expect that our Target won't be too deep in our Optional frame, but our frame can prepend elements or components with deep trees before the Target potentially leading us down wasteful rabbit holes.

So, I chose to do a breadth first search to quickly find the ,likely, shallow Target without going deep into sibling trees.

Identifying the Target
We can't rely on type or constructor names to look for our Target because minification or uglification obfuscates that data. We need to manually set a property that our BFS can check for to properly identify the target.

One way to do this, and ultimately, the method I chose, is to simply add a static property to the Target component function. We just need to ensure that the property name is unique enough to not interfere with other functionality.

The Code

function Target({ children }) {
  return children;
}

Target.optName = "Target";
Enter fullscreen mode Exit fullscreen mode
function Optional({ when, children }) {
  const target = findTarget(children);

  // Run a BFS to find Target component
  function findTarget(children) {
    let queue = Array.isArray(children) ? [...children] : [children];
    while (queue.length) {
      const current = queue.shift();
      if (current?.type?.optName === "Target") return current // Check static property

      // Add children to queue
      const children = current.props?.children;
      if (!children) continue;
      if (Array.isArray(children)) {
        queue.push(...children);
      } else {
        queue.push(children);
      }
    }
  }
  // Either return everything enclosed or just the Target
  return when ? children : target;
}
Enter fullscreen mode Exit fullscreen mode

And that's it. We can now declaratively wrap elements conditionally.

Here's our simple username example:

Further Discussion

This doesn't have to be the final form of our solution, we can define our target in even more ways and customize our approach.

Below I nested two optional wrappers with the main Target, a counter, furthest in. But if we want to be able to keep inner wrapper when we turn off the outer one, we need to make sure it's wrapped in a Target component as well.

<Optional when={showOuter}>
  <div className="outer">
    <Target>
      <Optional when={showInner}>
        <div className="inner">
          <Target>
             <Counter />
          </Target>
        </div>
      </Optional>
    </Target>
  </div>
</Optional>
Enter fullscreen mode Exit fullscreen mode

This quickly gets out of hand and is pretty ugly to boot. Is there another, more inline way to identify if an element is a Target? We can add a prop!

So, instead of wrapping our target elements in a component, we'll signify that its regular container isTarget by adding just that prop.

<Optional when={showOuter}>
  <div className="outer">
    <Optional when={showInner} isTarget>
      <div className="inner">
        <Counter isTarget />
      </div>
    </Optional>
  </div>
</Optional>
Enter fullscreen mode Exit fullscreen mode

Look much better. Then we simply update our BFS to check each child element for an isTarget prop, and pick the first one that has it as our new target.

Here's an example:

Realistically, we'd want to choose a more uncommon but still understandable prop if we're to use this approach to avoid interacting with a component's own functionality since we're passing props into them.

And that's all! Now we can conditionally wrap elements and components without pesky ternary operators and enjoy a more declarative feel to our JSX.

Top comments (0)