Building Components and JSX in React
Before you start
You should be familiar with React setup and understand how a build tool transforms source files before they reach the browser. This tutorial covers JSX syntax, component functions and props, conditional rendering, list keys, styling, and security — everything you need to start building React components with confidence.
What is JSX?
JSX (JavaScript XML) is a syntax extension for JavaScript that lets you write HTML-like markup directly inside JavaScript code. Building components with JSX is the core of React development: React components return JSX to describe what should appear on screen, and combining components with JSX is what gives React its declarative, composable style. Instead of calling DOM APIs to manually create and attach elements, you write declarative markup that looks familiar. Behind the scenes, a build tool like Babel or ESBuild transforms your JSX into plain JavaScript that the browser can execute.
// JSX: looks like HTML, runs in no browser directly
const heading = <h1 className="title">Hello, world!</h1>;
JSX is not a string. It does not get inserted as innerHTML. It compiles to JavaScript function calls, making it safe by default.
JSX expressions: embedding {}
Any valid JavaScript expression can be embedded inside JSX using curly braces {}. This includes variables, calculations, function calls, and ternary operators.
const name = "Alice";
const year = new Date().getFullYear();
const Greeting = () => (
<div>
<h1>Hello, {name}!</h1>
<p>The year is {year}.</p>
<p>2 + 2 = {2 + 2}</p>
</div>
);
Attributes in JSX
JSX attributes work differently from HTML. All attribute names use camelCase, and values are JavaScript expressions. This comes from JSX being closer to JavaScript’s DOM API than to HTML — element.className = 'box' is the JavaScript equivalent of class="box" in HTML, so JSX uses className. The same pattern applies to event handlers and boolean attributes:
// className instead of class
const Box = () => <div className="box">Content</div>;
// Boolean attributes require an explicit value
const Button = ({ disabled }) => (
<button disabled={disabled}>Press me</button>
);
// Dynamic attribute values
const DynamicImage = ({ src, alt }) => (
<img src={src} alt={alt} className="photo" />
);
How JSX gets compiled
JSX cannot run in a browser without transformation. A build tool like Babel, ESBuild, or SWC processes your .jsx files and converts every JSX element into a call to React.createElement(). This is a compile-time step, so the browser never sees JSX — it only receives the plain JavaScript output:
// What you write:
const element = <h1 className="title">Hello</h1>;
// What Babel/ESBuild outputs:
const element = React.createElement("h1", { className: "title" }, "Hello");
React.createElement() produces a plain JavaScript object, a virtual DOM node. React reads this description and constructs the real DOM from it.
You can write createElement calls directly without JSX, though the result is harder to read. The nesting mirrors the DOM tree structure, but the indentation and repeated function calls make it harder to scan than the equivalent JSX. For anything beyond a trivial element, JSX is the practical choice:
const element = React.createElement(
"div",
{ className: "container" },
React.createElement("h1", null, "Welcome"),
React.createElement("p", null, "No JSX required, but less pleasant.")
);
Modern React (17+) ships with the new JSX transform, which means you no longer need to write import React from 'react' in every file just because you use JSX. The transform handles it automatically.
Components as Functions
In modern React, a component is simply a JavaScript function that returns JSX. These are called functional components:
const Heading = () => {
return <h1>Hello, React</h1>;
};
const App = () => {
return (
<div>
<Heading />
</div>
);
};
Component names must start with a capital letter. React uses this to distinguish your custom components (<Heading />) from built-in HTML elements (<div />, <span />). Without the capital, React treats <heading /> as a generic HTML tag.
A component can only return one root element. If you need to return multiple siblings without wrapping them in a div, use a fragment <>...</>:
const Meta = () => (
<>
<title>Page Title</title>
<meta name="description" content="A page" />
</>
);
Props: passing data to components
Props are how you pass data into a component, functioning like arguments to a function. The parent component passes them as attributes in JSX, and the child receives them as a single object. This unidirectional flow keeps data predictable:
// Parent passes props
const App = () => {
return <Greet name="Charlie" score={85} />;
};
// Child receives props as its first argument
const Greet = (props) => {
return (
<p>
{props.name} scored {props.score}
</p>
);
};
Destructuring Props
Destructuring in the function signature is the cleanest way to extract props. It eliminates repetitive props. prefixes and makes it immediately clear which values a component expects. When a component accepts many props, destructuring also helps code review by listing dependencies right in the function header:
// Clean destructuring
const Greet = ({ name, score }) => (
<p>{name} scored {score}</p>
);
// Default values during destructuring
const Badge = ({ label, color = "blue" }) => (
<span style={{ backgroundColor: color }}>{label}</span>
);
// Collecting remaining props with spread
const Button = ({ label, ...rest }) => (
<button {...rest}>{label}</button>
);
Conditional Rendering
JSX has no if statements inside markup. Instead you use JavaScript expressions. React evaluates these expressions at render time, so you can use any JavaScript logic — short-circuit evaluation, ternaries, or helper functions — to decide what appears on screen. Conditional rendering changes what the user sees without mounting and unmounting components:
Logical AND (&&)
Renders the right side only when the condition is true:
const Notification = ({ message }) => {
return message && <p className="alert">{message}</p>;
};
Watch out for falsy values. If message is 0 or "", React renders the number or empty string. For numbers that might be zero, convert explicitly: {messages.length > 0 && ...}.
Ternary Operator
Use ? : when you need an if/else branch. Ternaries work inline within JSX, so you can place a branch directly inside an attribute or between tags without extracting the logic into a separate variable:
const Status = ({ isOnline }) => (
<span>{isOnline ? "🟢 Online" : "⚫ Offline"}</span>
);
Early Returns with null
Return null from a component when it has nothing to render. This pattern is cleaner than wrapping the entire JSX tree in a ternary because it keeps the main render path at the top level of the function, making the component easier to scan:
const PrivateMessage = ({ isAuthenticated, content }) => {
if (!isAuthenticated) return null;
return <p>{content}</p>;
};
This is cleaner than nesting ternary operators around the entire JSX tree.
Lists and Keys
Render arrays by mapping them to JSX elements. Always provide a key prop; React needs it to track which items changed efficiently. Without keys, React may reuse DOM nodes incorrectly when the list reorders, leading to visual glitches and stale state:
const ColorList = ({ colors }) => (
<ul>
{colors.map((color) => (
<li key={color}>{color}</li>
))}
</ul>
);
Keys must be unique among siblings and stable across renders. React uses keys to decide whether to reuse or replace a DOM node when the list changes. Prefer real IDs over array indexes when items can be reordered or removed. If an item shifts position, an index-based key will cause React to patch the wrong DOM node:
const users = [
{ id: 101, name: "Priya" },
{ id: 102, name: "Marcus" },
{ id: 103, name: "Yuki" },
];
const UserList = ({ users }) => (
<ul>
{users.map(({ id, name }) => (
<li key={id}>{name}</li>
))}
</ul>
);
If your data has no stable ID, the index is acceptable as a last resort, but be aware it will cause reconciliation bugs when the list order changes.
Component Composition
Composition is the practice of building complex UIs from small, single-responsibility components. A well-designed component does one thing well and accepts children to let parents inject content:
const Card = ({ children }) => (
<div className="card">{children}</div>
);
const Article = () => (
<Card>
<h2>Article Title</h2>
<p>Article body text goes here.</p>
</Card>
);
Composing with named props also works well for more structured layouts. Instead of relying on children for everything, you accept specific props and assemble them into a deliberate visual structure. This gives the parent component more control over the layout while keeping each child simple:
const Avatar = ({ src, alt }) => (
<img src={src} alt={alt} className="avatar" />
);
const Profile = ({ name, avatarUrl, bio }) => (
<div className="profile">
<Avatar src={avatarUrl} alt={name} />
<div>
<h3>{name}</h3>
<p>{bio}</p>
</div>
</div>
);
Styling in JSX
Two key differences from HTML: class is reserved in JavaScript, so use className. And all CSS property names use camelCase.
Inline Styles
Pass a JavaScript object to the style attribute. The object keys are camelCase CSS properties, and the values are strings. Inline styles are useful for dynamic values that change based on props or state, but for static styles, CSS Modules or a stylesheet are usually a better choice:
const StyledBox = () => (
<div style={{
backgroundColor: "#f5f5f5",
padding: "20px",
borderRadius: "8px"
}}>
Inline styles
</div>
);
CSS Modules
CSS Modules scope class names to the component, preventing accidental collisions across your app. Each .module.css file exports an object mapping local class names to unique generated identifiers, so two components can use .btn without conflict. The build tool handles the scoping at compile time:
// Button.module.css
// .btn { padding: 8px 16px; }
// .primary { background: #3b82f6; }
import styles from "./Button.module.css";
const Button = ({ variant = "primary", label }) => {
const cls = variant === "secondary" ? styles.secondary : styles.primary;
return <button className={`${styles.btn} ${cls}`}>{label}</button>;
};
JSX vs HTML: key differences
| Feature | HTML | JSX |
|---|---|---|
| Class attribute | class="..." | className="..." |
Label for attribute | for="..." | htmlFor="..." |
| Event handlers | onclick="..." | onClick={...} |
| Self-closing tags | <br>, <img> | Must use <br />, <img /> |
| Boolean attributes | Present or absent | Must set value: disabled={true} |
| Root element | Multiple siblings allowed | Single root element (or use fragments) |
| Dynamic values | String concatenation | Embed expressions with {} |
JSX also enforces that all attribute names use camelCase. onclick is wrong, onClick is correct. Misspelling an attribute name produces no error at compile time, but the event will simply never fire.
JSX and Security
One of JSX’s strongest features is automatic escaping. Any value embedded in {} is converted to a string and escaped before being inserted into the DOM. This makes XSS attacks through user input much harder. React treats interpolated values as text, never as executable code:
const Comment = ({ text }) => <p>{text}</p>;
// text = '<script>stealCookies()</script>'
// React renders it as harmless escaped text, not executable HTML
If you truly need to render raw HTML (and you almost never should), React exposes dangerouslySetInnerHTML. Using it bypasses the escaping layer entirely. Treat it as a last resort with untrusted data. When you must use it, pair it with a sanitizer like DOMPurify:
const RawHTML = ({ html }) => (
<div dangerouslySetInnerHTML={{ __html: html }} />
);
Why JSX helps teams
JSX gives React code a shared visual language. When a teammate opens a component file, they can spot structure, props, and conditionals without mentally translating a stream of createElement calls. That matters in reviews because the intended UI shape is visible right away. JSX also keeps the component tree close to the code that drives it, which makes refactors less risky. You can move a section, rename a prop, or split a component with a clearer sense of what changed.
Keep components small
Small components are easier to test and easier to reuse. If a component starts to manage too many branches, it usually wants to be broken into a layout component and one or more focused children. That split makes the data flow clearer and helps you keep state near the part of the tree that actually needs it. It also makes composition feel natural, because the parent passes content instead of trying to own every detail itself.
Keep the component tree readable
A React tree is easier to maintain when each component has one clear job. If a file mixes layout, data fetching, and event handling, it becomes hard to tell which changes belong together. Splitting those concerns makes the JSX easier to scan and gives future edits a smaller blast radius. That matters in larger apps, where a tidy component hierarchy saves time every time someone comes back to a file after a few weeks away.
Next steps
- Apply these patterns by building a small dashboard component that accepts data as props and renders a list of cards — this exercises composition, keys, and conditional rendering in one compact exercise
- Move on to React Props and State to learn how components manage interactive data
- Read React Context and Reducers for scaling state management beyond props
See also
- React: Introduction and Setup: get React running in your project from scratch
- JavaScript Classes: understand the class syntax that older React components rely on