It really grinds my gears when I see this pattern, in particular navigation bars and sidebars seem to attract it:
const navItems = [
{
title: 'home',
link: '/home',
},
{
title: 'about',
link: '/about',
},
{
title: 'contact us',
link: '/contact-us',
},
// ... whatever ...
];
and then later
<ul>
{navItems.map(({ link, title }) => {
return (
<li>
<Link to={link}>{title}</Link>
</li>
);
})}
</ul>
This is totally unnecessary.
The implementer of the nav bar could directly unroll the array:
<ul>
<li><Link to="/home">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/contact-us">Contact Us</Link></li>
</ul>
For one, it's a lot less code! The unrolled version conveys the same information as the list but instead of adding a level of indirection, it directly codifies what is desired by the developer which makes it much simpler to make changes.
For two, the JSX variant makes it much easier to incorporate element-specific logic. Say, for example, you want a conditional on only one nav item. With an array, you'll have to figure out how to mutate the array or conditionally produce it. JSX already has this baked in!
<ul>
<li><Link to="/home">Home</Link></li>
{showAbout && <li><Link to="/about">About</Link></li>}
<li><Link to="/contact-us">Contact Us</Link></li>
</ul>
There's no need to make your life harder for no reason. The problem is further exacerbated when these conditions are produced in hooks and I've seen some developers go as far as writing a function that produces an array of items that is then converted to JSX.
function getNavItems(someCondition: boolean) {
return [
{
title: 'home',
link: '/home',
},
{
title: 'about',
link: '/about',
},
{
title: 'contact us',
link: '/contact-us',
},
// ... whatever ...
];
}
// ... later ...
function NavComponent() {
const someCondition = useSomeHook();
const navItems = getNavItems(someCondition);
return <ul>
{navItems.map(({ link, title }) => {
return (
<li>
<Link to={link}>{title}</Link>
</li>
);
})}
</ul>;
}
A function that produces nav items... that almost sounds like a React component! So not only have we reinvented JSX, we've completely reinvented the component model except on top of React, so we have to convert from one component model to another.
I suspect this antipattern comes from the desire to not hardcode strings. I've seen a lot of developers go through some herculean efforts to avoid hardcoding strings and by extension, an array of objects that we convert to JSX is "more maintainable" than JSX itself.
Except therein lies the problem: React and JSX are declarative languages already.
The desire to not hardcode strings, in my opinion, is a desire to keep imperative logic simpler and move constants to a more declarative world to add semantic information. That makes total sense. What doesn't make sense is moving declarative logic from a declarative world to another declarative world. JSX is already sufficiently descriptive and it's in some sense a giant constant to begin with. Embrace it!
There are cases where creating an abstract list does make more sense than using JSX because JSX isn't a universal hammer.
For example, in React Router the use of
data APIs is essentially moving away from the JSX'ified <Route>
pattern.
But this is still in the spirit of the law! The routes here don't represent a list of UI elements, they represent an abstract concept so there is no need to convert it back to React. Whereas in our navigation example, the items directly correspond to UI elements. In that case, React already dictates JSX as the interface. We don't need to create yet another layer of abstraction because we have a perfectly suitable (and also mandated by the framework) one in front of us: JSX.
Provide a context then.
As you said: Reduce duplication using components.
Menu entries are not routes not only in that there may be routes that are not referenced by any entry but also in that there may be entries that do not reference any routes. This is a mistake hampering your ability to create highly purpose-build user interfaces.
Menu entries are not routes. If you need to dynamically look up information about routes, do that. Frameworks like NextJS or Remix do some of that for you but you can implement it yourself. In React Router you could use the "handle" attribute to make this type of information available.