ESC

Type to search the knowledge base.

JSX Under the Hood

JSX compiles to createElement or jsx() calls: elements as objects, props and children, and why type plus key drive identity.

beginner3 min read
  • react
  • jsx-under

JSX is syntax sugar, not a browser feature. Build tools turn it into function calls that produce element objects — plain descriptions React later reconciles to the DOM (or native views).

const el = <button type="button" className="primary">Save</button>;

With the classic runtime this is roughly:

const el = React.createElement(
  'button',
  { type: 'button', className: 'primary' },
  'Save'
);

The modern automatic runtime (react/jsx-runtime) emits jsx / jsxs instead of requiring React in scope for every file — same idea: type, props, children.

Docs: Writing Markup with JSX, createElement.

Elements are blueprints

Conceptually an element looks like:

{
  type: 'button',
  props: { type: 'button', className: 'primary', children: 'Save' },
  key: null,
  ref: null,
}
  • type: string for host components (div), function/class for composites, or special types (fragments).
  • props: inputs, including children.
  • key / ref: reserved by React; not ordinary props your component reads the same way.

Changing type on the next render tells React to tear down the old subtree and mount a new one — the core of reconciliation.

Expressions and children

Curly braces embed JS expressions:

function Price({ cents, currency }) {
  const label = new Intl.NumberFormat(undefined, {
    style: 'currency',
    currency,
  }).format(cents / 100);
  return <span>{label}</span>;
}

Children can be text, elements, arrays, or null / false / undefined (which render nothing):

{isAdmin && <AdminPanel />}
{items.length ? <List items={items} /> : null}

Trap: 0 is a valid React child and will render. Prefer an explicit ternary when the left side of && can be 0.

Props quirks worth memorizing

HTML / DOM In JSX
class className
for htmlFor
inline styles object with camelCase: style={{ marginTop: 8 }}
boolean attributes disabled or disabled={true}
events camelCase: onClick, onChange
<input
  className="field"
  style={{ width: '100%' }}
  onChange={(e) => setValue(e.target.value)}
/>

Self-closing tags are required when there are no children: <img src={url} alt="" />.

Components as types

function Hello({ to }) {
  return <p>Hello {to}</p>;
}

const node = <Hello to="Ada" />;
// type is the Hello function

Dynamic tags need a capitalized variable:

const Tag = as === 'h1' ? 'h1' : 'h2';
return <Tag className="title">{children}</Tag>;

Interview out-loud

“JSX compiles to createElement or jsx calls that return element objects with type, props, and children. React reconciles those descriptions to the host tree. Keys and component type drive identity. className and htmlFor replace class and for because JSX is JavaScript.”

Further reading

Edge cases worth rehearsing

Interviewers and production incidents cluster around the same edges: first render versus update, empty and loading states, Strict Mode double setup, concurrent interruptions, and what happens when identity (key, route params, user id) changes mid-edit. Walk one concrete user journey end-to-end — open, edit, navigate away, come back — and say which state survives.

Prefer fixing data flow and ownership before reaching for memoization or micro-optimizations. Prefer event handlers over effects when a user action is the trigger. Prefer deriving values during render over mirroring props into state. Prefer stable list keys from business ids. Measure with the profiler when performance is the claim.

When you cite an API, mention one failure mode: abort on unmount, serializable props across server/client boundaries, focus restoration for dialogs, or cache invalidation after a mutation. Specific beats generic every time.

Related guides