Home / Articles

From JSX to Plain Objects: How React Elements, Components and Props Work

See what JSX compiles to, why components are just functions, and how props, children, defaults, spreading and slots follow from that one mental model.

Publisher: REACTAPP.TOP9 min read1983 words

This article is published in English.

Sources: JSX, Components & Props — The Mental Model Every React Developer Actually Needs

Many React bugs come from a fuzzy picture of what JSX, components and props actually are. Once you know that JSX becomes a function call, that the call produces a plain object, and that a component is a function receiving one object of arguments, rules that seem arbitrary start to make sense. This article builds that model from the compiler output upward, then applies it to props, children, defaults, spreading, immutability and composition patterns you will use every day.

JSX is function calls, not HTML

JSX is often called "HTML inside JavaScript", which is incomplete. More precisely, JSX is syntactic sugar that a compiler turns into ordinary function calls.

Take a simple heading:

const element = <h1 className="title">Hello World</h1>;

A compiler such as Babel turns it into a call to React.createElement, passing the tag name, an object of attributes and the content:

const element = React.createElement("h1", { className: "title" }, "Hello World");

This is the classic transform. Since React 17, most toolchains use the automatic JSX runtime, which emits calls to a jsx() function imported from react/jsx-runtime instead, so you no longer need React in scope. The idea is identical: markup becomes a function call.

That call returns a plain JavaScript object called a React element:

{
  type: "h1",
  props: {
    className: "title",
    children: "Hello World"
  }
}

A React element is nothing more than a description of what should appear on screen. React walks the tree of these objects and works out how to bring the real DOM in line with it; that tree of descriptions is what people usually mean by the Virtual DOM.

What the model explains

Holding onto "JSX becomes a function call that returns an object" clears up several common puzzles:

  • No if statements inside JSX. You cannot put a statement inside a function argument, so you use a ternary or && expression, or move the logic above the return.
  • className instead of class. JSX is JavaScript, and class is a reserved word there.
  • A single root element. A function returns one value, so a component must return one element (which may contain many children).

A component is a function that returns elements

Components are functions whose return value is a React element, or null to render nothing.

function Greeting() {
  return <h1>Hello, Devendra!</h1>;
}

There is no hidden machinery and, in modern React, no need for classes. React calls the function whenever it needs to know what to render. When data changes, React calls it again, receives a new element tree, compares it with the previous one, and updates only the parts of the DOM that differ. That comparison is called reconciliation.

Two rules every component follows

Names must start with a capital letter

JSX uses capitalization to decide what a tag means. A capitalized tag refers to a component, so React calls your function:

// ✅ React treats this as a component
<Greeting />

A lowercase tag is treated as a built-in DOM element, so React would try to create a literal <greeting> element instead of calling anything:

// ❌ React treats this as a native DOM element (like <div>)
<greeting />

Return exactly one root

Returning two sibling elements side by side fails, because it amounts to returning two values from one function:

// ❌ This will throw an error
function Card() {
  return (
    <h2>Title</h2>
    <p>Description</p>
  );
}

There are two fixes. Wrap the siblings in a parent element, or use a Fragment, written <>...</>, which groups them without adding an extra node to the DOM. The snippet below shows both versions back to back.

// ✅ Wrap in a parent
function Card() {
  return (
    <div>
      <h2>Title</h2>
      <p>Description</p>
    </div>
  );
}// ✅ Or use a Fragment (renders nothing extra in the DOM)
function Card() {
  return (
    <>
      <h2>Title</h2>
      <p>Description</p>
    </>
  );
}

Prefer the Fragment unless the wrapper is needed for styling or layout.

Props are the function's arguments

If components are functions, props are their arguments. Here the component reads a name value from the object it receives:

function Greeting(props) {
  return <h1>Hello, {props.name}!</h1>;
}

And here it is rendered with that attribute set:

// Usage
<Greeting name="Devendra" />

React gathers every attribute written on the JSX tag into one object, props, and passes it as the first argument. That is the whole mechanism.

Destructure in the signature

Repeating props. quickly gets noisy. Destructuring the object in the parameter list names each prop you use and documents the component's inputs at a glance:

function Greeting({ name, role }) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>Role: {role}</p>
    </div>
  );
}

The call site is unchanged:

<Greeting name="Devendra" role="Senior Engineer" />

Default values with destructuring

When a prop is optional, give it a default directly in the destructuring pattern. If role is not supplied, it falls back to "Developer":

function Greeting({ name, role = "Developer" }) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>Role: {role}</p>
    </div>
  );
}

So this usage renders the default role:

// role will be "Developer" if not provided
<Greeting name="Devendra" />

Destructuring defaults apply only when the value is undefined. Passing null explicitly bypasses the default, which is worth remembering when props come from API data.

children: composition built into the syntax

Content nested between a component's opening and closing tags arrives automatically as the children prop. A Card can simply render whatever it is given inside its wrapper:

function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

The caller decides what goes inside:

// Usage
<Card>
  <h2>My Title</h2>
  <p>Some description here.</p>
</Card>

Composition through children is how flexible, reusable components are built. Rather than anticipating every variation with another prop, the component provides structure and the parent supplies the content. Libraries such as shadcn/ui and Material UI build dialogs, cards and buttons this way.

Any JavaScript value can be a prop

Curly braces in JSX mean "evaluate this as JavaScript", so a prop can hold any value: strings, numbers, booleans, arrays, objects or functions. This component receives several kinds at once, rendering a badge conditionally, mapping an array into list items and wiring a callback to a button:

function UserCard({ name, age, isAdmin, hobbies, onDelete }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
      {isAdmin && <span>Admin</span>}
      <ul>
        {hobbies.map(h => <li key={h}>{h}</li>)}
      </ul>
      <button onClick={onDelete}>Delete</button>
    </div>
  );
}

The call site shows how each kind is passed:

<UserCard
  name="Devendra"
  age={28}
  isAdmin={true}
  hobbies={["coding", "teaching", "gaming"]}
  onDelete={() => console.log("deleted")}
/>

Points to notice:

  • Plain string values can be written in quotes directly.
  • Everything else, including numbers, booleans, arrays, objects and functions, goes in curly braces.
  • A function prop such as onDelete is how a child reports events back to its parent. This callback pattern is what makes lifting state up work: the state lives in a shared parent, and children change it only by calling functions the parent passes down.

Props are read-only

Never mutate props. Assigning to a field of a prop object silently changes data the parent owns:

// ❌ Never do this
function Greeting({ user }) {
  user.name = "Modified"; // This will cause silent bugs
  return <h1>{user.name}</h1>;
}

Derive a new value instead and leave the original untouched:

// ✅ Create a new value instead
function Greeting({ user }) {
  const displayName = user.name.toUpperCase();
  return <h1>{displayName}</h1>;
}

Data flows one way, from parent to child, which is what keeps React apps predictable. A child that needs the parent to change something calls a function the parent provided.

Forwarding props with rest and spread

When a component wraps a DOM element, you often want to accept many attributes without listing each one. Rest syntax in the parameter pulls out the props the component handles itself, here label, and collects the remainder into inputProps, which is then spread onto the input:

function Input({ label, ...inputProps }) {
  return (
    <div>
      <label>{label}</label>
      <input {...inputProps} />
    </div>
  );
}

Every extra attribute, such as type, placeholder or onChange, passes straight through to the underlying element:

// All extra props (type, placeholder, onChange, etc.) flow straight to <input>
<Input
  label="Email"
  type="email"
  placeholder="you@example.com"
  onChange={handleChange}
/>

Libraries use this pattern constantly, but spreading arbitrary props onto a DOM element can forward values it does not understand, triggering unknown-attribute warnings. Be deliberate about what you spread and where. The example also renders a label without linking it to the input; in real code, connect them with htmlFor and an id so the label is accessible.

Three levels of component flexibility

Components tend to evolve through recognizable stages as requirements grow.

Level 1: a prop for every variation

The simplest button accepts a separate prop for each thing it might show:

function Button({ text, color, icon }) {
  return <button style={{ color }}>{icon} {text}</button>;
}

Every new variation needs yet another prop, and the API grows without limit. This problem is often called prop explosion; it is distinct from prop drilling, which is passing data down through many layers.

Level 2: children composition

Letting callers pass content as children removes most of those props. The button keeps only a variant for styling:

function Button({ children, variant }) {
  return <button className={`btn btn-${variant}`}>{children}</button>;
}

The caller now controls what appears inside, including icons:

<Button variant="primary">
  <Icon name="save" /> Save Changes
</Button>

Level 3: named slots

When a component needs several distinct regions, accept JSX through named props. A page layout can take a header, a sidebar and main content:

function PageLayout({ header, sidebar, children }) {
  return (
    <div className="layout">
      <header>{header}</header>
      <aside>{sidebar}</aside>
      <main>{children}</main>
    </div>
  );
}

Each region is filled at the call site:

<PageLayout
  header={<Navbar />}
  sidebar={<FilterPanel />}
>
  <ProductList />
</PageLayout>

Slots are ordinary props holding JSX: callers control what fills each area, the component controls structure, and the layout survives changing requirements. For a deeper look at this refactoring path, see fixing React prop overload with composition and slots.

The model in brief

  • JSX: syntactic sugar for element-creating function calls such as React.createElement().
  • React element: a plain JavaScript object describing UI.
  • Component: a function that returns React elements.
  • Props: the single object of arguments passed to that function.
  • children: a special prop holding nested JSX content.
  • Prop mutation: never allowed; treat props as read-only.

Purity is the rule that scales

All of this depends on purity: the same props should always produce the same output. When rendering has no side effects, never mutates props and does not modify external state, React can safely skip, repeat and reorder work, and you can reason about each component in isolation. Strict Mode even renders components twice in development to expose impure logic.

Break purity and you invite bugs that surface far from their cause. Keep it, and your components stay predictable, easy to test and maintainable as the application grows.