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".You'll return a { type, key, props } object: props (defaulted to {}) carries everything including children, and key — passed as the third argument — is stringified into its own field.
The automatic JSX runtime moves two responsibilities out of your markup and into a function. First, children go into props: <div>hi</div> becomes jsx('div', { children: 'hi' }), so unlike the classic createElement(type, props, ...children) you don't collect trailing arguments — the compiler already nested them under props.children. Second, key is special: it's not a normal prop (React uses it for reconciliation, never passes it to your component), so the transform hands it as a separate third argument, and you store it as element.key. Everything else is a straight passthrough.
jsx is the tiny factory your compiled markup calls. Given type (a tag string, a component function, or Fragment), props (all attributes plus children), and an optional key, it packages them: type as-is, props as-is (or {} if omitted), and key normalized to a string (or null). jsxs is the exact same function under a different name — the compiler picks jsxs when it knows the children are a static array, purely as a hint; you don't have to do anything different. Fragment is just a unique marker value used as type.
The naive version copies the classic createElement signature and mishandles children/key:
function jsxNaive(type, props, ...children) { // wrong arity for the automatic runtime
return {
type,
props: { ...props, children }, // children are ALREADY in props here
key: props.key, // leaves key inside props too
};
}
Two mistakes. The automatic runtime does not pass children as rest arguments — they're already in props.children, so collecting ...children grabs the key (the real third arg) as a "child". And reading props.key leaves key sitting in props, which pollutes the props your component receives (and key should never reach it). The correct signature is jsx(type, props, key) with children left inside props.
const Fragment = Symbol('jsx.Fragment');
function jsx(type, props, key) {
return {
type,
key: key === undefined ? null : String(key), // separate field, stringified
props: props || {}, // children already live here
};
}
const jsxs = jsx; // identical; the compiler uses jsxs for static-array children
module.exports = { jsx, jsxs, Fragment };
jsx takes exactly three parameters. type passes straight through — it can be a string ('div'), a component function, or Fragment. props is returned as-is (defaulted to {}), so children and every attribute stay together, exactly as the compiler arranged them. key — the third argument — is normalized: null when absent, otherwise String(key) (React keys are always strings). Crucially, key is not spread into props, so a component never sees it. jsxs is bound to the same function; Fragment is a unique Symbol a renderer can check against.
The compiler turns <li key={7}>item</li> into jsx('li', { children: 'item' }, 7):
type — 'li', stored directly.props — { children: 'item' }, stored as-is (children are already inside).key — the third arg 7; String(7) → '7', stored as element.key. It does not appear in props.{ type: 'li', key: '7', props: { children: 'item' } }. A renderer reads type to make the element, props.children to fill it, and key (outside props) only for list reconciliation.props.children; a ...children param would swallow the key argument instead.key in props — key must be a separate field and kept out of props, or it leaks into the component's props (React explicitly forbids reading props.key).String(key) avoids 7 !== '7' surprises during reconciliation.jsx vs jsxs — they're the same function; jsxs is only a compiler hint for static children, not different behavior.jsxDEV — the development runtime adds a fourth argument with source location and self/source for better error messages; production uses jsx/jsxs.React.createElement(type, props, ...children) (children as rest args, no separate key slot) is what pragma-based setups still emit; both produce the same shape of element.render/reconciler (the virtual-DOM questions) closes the loop from <JSX/> all the way to real DOM.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
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".You'll return a { type, key, props } object: props (defaulted to {}) carries everything including children, and key — passed as the third argument — is stringified into its own field.
The automatic JSX runtime moves two responsibilities out of your markup and into a function. First, children go into props: <div>hi</div> becomes jsx('div', { children: 'hi' }), so unlike the classic createElement(type, props, ...children) you don't collect trailing arguments — the compiler already nested them under props.children. Second, key is special: it's not a normal prop (React uses it for reconciliation, never passes it to your component), so the transform hands it as a separate third argument, and you store it as element.key. Everything else is a straight passthrough.
jsx is the tiny factory your compiled markup calls. Given type (a tag string, a component function, or Fragment), props (all attributes plus children), and an optional key, it packages them: type as-is, props as-is (or {} if omitted), and key normalized to a string (or null). jsxs is the exact same function under a different name — the compiler picks jsxs when it knows the children are a static array, purely as a hint; you don't have to do anything different. Fragment is just a unique marker value used as type.
The naive version copies the classic createElement signature and mishandles children/key:
function jsxNaive(type, props, ...children) { // wrong arity for the automatic runtime
return {
type,
props: { ...props, children }, // children are ALREADY in props here
key: props.key, // leaves key inside props too
};
}
Two mistakes. The automatic runtime does not pass children as rest arguments — they're already in props.children, so collecting ...children grabs the key (the real third arg) as a "child". And reading props.key leaves key sitting in props, which pollutes the props your component receives (and key should never reach it). The correct signature is jsx(type, props, key) with children left inside props.
const Fragment = Symbol('jsx.Fragment');
function jsx(type, props, key) {
return {
type,
key: key === undefined ? null : String(key), // separate field, stringified
props: props || {}, // children already live here
};
}
const jsxs = jsx; // identical; the compiler uses jsxs for static-array children
module.exports = { jsx, jsxs, Fragment };
jsx takes exactly three parameters. type passes straight through — it can be a string ('div'), a component function, or Fragment. props is returned as-is (defaulted to {}), so children and every attribute stay together, exactly as the compiler arranged them. key — the third argument — is normalized: null when absent, otherwise String(key) (React keys are always strings). Crucially, key is not spread into props, so a component never sees it. jsxs is bound to the same function; Fragment is a unique Symbol a renderer can check against.
The compiler turns <li key={7}>item</li> into jsx('li', { children: 'item' }, 7):
type — 'li', stored directly.props — { children: 'item' }, stored as-is (children are already inside).key — the third arg 7; String(7) → '7', stored as element.key. It does not appear in props.{ type: 'li', key: '7', props: { children: 'item' } }. A renderer reads type to make the element, props.children to fill it, and key (outside props) only for list reconciliation.props.children; a ...children param would swallow the key argument instead.key in props — key must be a separate field and kept out of props, or it leaks into the component's props (React explicitly forbids reading props.key).String(key) avoids 7 !== '7' surprises during reconciliation.jsx vs jsxs — they're the same function; jsxs is only a compiler hint for static children, not different behavior.jsxDEV — the development runtime adds a fourth argument with source location and self/source for better error messages; production uses jsx/jsxs.React.createElement(type, props, ...children) (children as rest args, no separate key slot) is what pragma-based setups still emit; both produce the same shape of element.render/reconciler (the virtual-DOM questions) closes the loop from <JSX/> all the way to real DOM.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.