JSX isn't magic — it's syntax sugar that a compiler rewrites into plain function calls. The modern "automatic runtime" (React 17+, and what Babel/SWC emit by default) compiles <div id="a">hi</div> into jsx('div', { id: 'a', children: 'hi' }). Implementing jsx (and its siblings jsxs and Fragment) shows you exactly what your markup becomes: a plain object describing type, props, and a key.
Implement jsx(type, props, key), jsxs (its alias for static-array children), and a Fragment symbol. Return a vnode { type, key, props } — with children living inside props.children, and key pulled out as a separate field.
function jsx(type, props, key) {
// returns { type, key, props } (props.children holds the children)
}
// <a href="/x">go</a> compiles to:
jsx('a', { href: '/x', children: 'go' });
// -> { type: 'a', key: null, props: { href: '/x', children: 'go' } }
// <li key="row-1">x</li> compiles to:
jsx('li', { children: 'x' }, 'row-1'); // key is the 3rd arg
createElement(type, props, ...children), the automatic runtime puts children inside props.children. Don't pull them out.key is a separate arg — the transform passes key as the third argument, not in props; store it as element.key (stringified), and keep it out of props.jsxs === jsx — the transform calls jsxs when the children are a static array (an optimization hint); behavior is identical.Fragment — a unique symbol used as the type for <>…</>; a renderer treats it as "just my children, no wrapper element".Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.