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.
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>
)
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>
)
I liked this approach a lot, and it definitely felt, very much, within reach.
The Plan:
- Define an
Target
component which simply returns itschildren
- Define an
Optional
component, which finds a nestedTarget
component, and depending on itswhen
prop, either render just theTarget
or all itschildren
.
The tricky part was in step 2, namely finding the nested Target
.
To do it, we needed to:
- Find a way to search through the
Optional
component's children efficiently. - 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";
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;
}
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>
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>
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)