The "Component Switch" Pattern I Use Instead of Big Render Trees

Big if/else ladders that pick a component by a tag are unreadable by the third branch. The pattern I use instead is a tiny lookup registry. Three accordions on shipping it.

JavaScript
Frontend
3 snippets
react
hooks
design-patterns
diegonguyen

By @diegonguyen

April 9, 2026

·

Updated August 9, 2026

781 views

25

4.5 (8)

// The smallest version of the pattern. A plain object whose keys are the
// shape tags and whose values are the components, plus a switch component
// that looks up the entry and spreads the rest of the props through. That is
// it. Less than 10 lines of glue, and it scales linearly with the number of
// tags rather than nesting.
const { createElement } = (typeof React !== 'undefined' ? React : {
    createElement: (type, props, ...kids) => ({ type, props: props || {}, children: kids }),
});

// Three real components. In practice they live in their own files.
function CardView(props) {
    return createElement('div', { className: 'card' }, 'card[' + props.title + ']');
}
function ListView(props) {
    return createElement('ul', null, ...props.items.map((it) => createElement('li', null, it)));
}
function GridView(props) {
    return createElement('div', { className: 'grid' }, 'grid of ' + props.items.length + ' tiles');
}

// The registry. New view = add one line.
const registry = {
    card: CardView,
    list: ListView,
    grid: GridView,
};

function ComponentSwitch(props) {
    const { tag } = props;
    const Found = registry[tag];
    if (!Found) return null;
    // Pass everything except `tag` through. Spread keeps the call site honest.
    const rest = Object.assign({}, props);
    delete rest.tag;
    return createElement(Found, rest);
}

console.log(JSON.stringify(ComponentSwitch({ tag: 'card', title: 'Hello' }), null, 2));
console.log(JSON.stringify(ComponentSwitch({ tag: 'list', items: ['a', 'b', 'c'] }), null, 2));
console.log(JSON.stringify(ComponentSwitch({ tag: 'grid', items: [1, 2, 3, 4] }), null, 2));
console.log('unknown tag returns null:', ComponentSwitch({ tag: 'nope', items: [] }));

Once I started writing this pattern, I stopped writing the if/else version. The registry is the entire surface area: every new view type is one line in the object, and the switch component has no behaviour beyond "look it up, render it, pass the rest of the props". Spreading the props rather than enumerating them is what keeps the switch generic; if a future view needs an extra prop, you just add it at the call site. The null return on an unknown tag is a deliberate choice: silently rendering nothing is the right behaviour during a feature flag rollout where the new tag has not landed yet, and the next accordion handles the strict variant for cases where you want a hard error instead.