089: React Setup + JSX
Learning objective
Outcomes
You will create a current Vite React client, follow its entry point into the application, write function components and JSX expressions, use Fragments, and apply the rules that make JSX valid and predictable.
By the end, I can start the app, explain the purpose of every starter file, and render an accessible static Task Manager with the current root API.
Prerequisites
Complete 088 first. You should have Node.js and npm available, be comfortable reading JavaScript imports and exports, and be able to run terminal commands. React does not need to be installed globally.
Retrieval practice
- What is the difference between rendering and committing?
- Why should component rendering be pure?
- Which component should own data used by two sibling branches?
Content to cover
project structure; JSX expressions; components; rendering; fragments; rules of JSX.
Terms and mental model
When JSX first appears, it is easy to mistake it for HTML embedded in a JavaScript file. JSX is actually a JavaScript syntax extension. A build transform converts it into JavaScript that creates React elements. It is neither an HTML string nor a DOM node. Curly braces provide the deliberate boundary from JSX into JavaScript expressions.
- Vite: A build tool and development server that provides fast feedback, including instant HMR, for React projects. — Source: Vite: Getting started
- Module: A file with private scope that connects to other files through imports and exports. — Source: MDN: JavaScript modules
- Root: The
createRootbinding that tells React which DOM node it is responsible for managing. — Source: React: createRoot - React element: The plain object description of UI produced by JSX before React renders it. — Source: React: Describing the UI
- Fragment: A wrapper that groups children in the React tree without adding a DOM node (
<></>). — Source: React: Fragment - HMR: Hot Module Replacement. Vite swaps edited modules in place, usually preserving component state while you work. — Source: Vite: Getting started
As of 2026-08-24, the official sources checked for this lesson list React 19.2 (npm latest react 19.2.8) and Vite 8.2.2. Those numbers document the versions checked here; they are not a reason to hard-code a patch version in your project. Use the official moving command, npm create vite@latest, and let the generated lockfile record the exact packages installed. According to its official guide, Vite 8 requires Node 20.19+ or 22.12+.
Setup
Create the project, install its dependencies, and start the development server:
npm create vite@latest task-manager -- --template react
cd task-manager
npm install
npm run dev
Do not use Create React App for this exercise. Vite gives this curriculum a small client-only environment without adding server rendering or unrelated infrastructure. npm run build produces production assets, and npm run preview serves those built assets locally so you can check the result.
These are the files you need to be able to locate immediately:
task-manager/
├─ index.html browser entry with <div id="root">
├─ package.json dependencies and scripts
├─ vite.config.js Vite/plugin configuration
├─ public/ files copied as-is
└─ src/
├─ main.jsx React root entry
├─ App.jsx top application component
└─ index.css imported styles
Vite treats the root-level index.html as source, not merely as an inert page template. It follows that file's module script to src/main.jsx, and the imports from there establish the rest of the module graph.
Beginner complete example
Replace src/main.jsx with this root entry:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './index.css';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
);
Replace src/App.jsx with this small component tree:
const user = 'Suriya';
const openTasks = 2;
function Header() {
return (
<header>
<p className="eyebrow">Workspace</p>
<h1>{user}'s Task Manager</h1>
</header>
);
}
function TaskPreview() {
return (
<>
<h2 id="today-heading">Today</h2>
<ul>
<li>Review JSX rules</li>
<li>Build component tree</li>
</ul>
</>
);
}
export default function App() {
return (
<main className="app-shell">
<Header />
<p>{openTasks === 1 ? '1 open task' : `${openTasks} open tasks`}</p>
<section aria-labelledby="today-heading">
<TaskPreview />
</section>
</main>
);
}
The heading ID is not decorative: it makes the section's aria-labelledby reference point to a real heading. Replace src/index.css with the following minimal styles:
:root { font-family: system-ui, sans-serif; color: #17202a; background: #f4f1ea; }
body { margin: 0; }
.app-shell { width: min(42rem, 90%); margin: 3rem auto; }
.eyebrow { color: #52606d; text-transform: uppercase; letter-spacing: .12em; }
li { margin-block: .6rem; }
createRoot receives the actual #root DOM node. root.render receives JSX, here <App />; it does not receive the App function itself. Strict Mode is a development aid that helps expose impure rendering and effect-cleanup bugs. Its development checks do not mean that the same extra behavior is part of the production runtime.
JSX rules
These rules prevent most first-pass JSX errors:
- Return one root node. Use a semantic wrapper or
<>...</>when the siblings do not need a new DOM element. - Close every tag:
<img />,<input />, and<li>...</li>are all required forms. - Most DOM property names use camelCase:
className,htmlFor, andonClickare examples. - Put JavaScript expressions, not statements, inside
{}.{task.title}and{condition ? a : b}work;{if (...)}does not. - Component names begin with a capital letter. Lowercase names refer to built-in DOM elements.
- JSX comments use
{/* comment */}. - Style objects use JavaScript property names and values:
style={{ backgroundColor: '#fff' }}.
Curly braces can contain variables, function calls, arithmetic, property access, arrays of nodes, and conditional expressions. An object cannot be rendered directly as a child. Booleans, null, and undefined render nothing, which is useful for conditional output but can also explain an unexpectedly empty area.
Intermediate: modules and composition
Create src/components/TaskHeader.jsx:
export default function TaskHeader({ owner }) {
const today = new Intl.DateTimeFormat(undefined, {
dateStyle: 'full',
}).format(new Date());
return (
<header>
<h1>{owner}'s Task Manager</h1>
<p>{today}</p>
</header>
);
}
Then import it with the exact path and extension:
import TaskHeader from './components/TaskHeader.jsx';
export default function App() {
return (
<main>
<TaskHeader owner="Suriya" />
<h2>Today</h2>
</main>
);
}
The owner value is a prop, and the component reads it through parameter destructuring. Keep related small components in one file at first. Split a file when navigation or reuse makes that useful, not because every component is required to live in its own file.
Optional advanced: JSX transform and root details
Modern JSX setups do not require import React from 'react' just because a file contains JSX. Hooks and named React APIs still need their imports. Vite's React plugin transforms JSX and supports development refresh. A client-rendered application usually has one root. Server-rendered markup uses hydrateRoot instead; this Vite client starts with an empty root, so createRoot is the correct API.
Mistakes and debugging
- Blank page: read both the terminal and the browser console. A syntax or import error can stop rendering before any UI appears.
Target container is not a DOM element: verify thatindex.htmlcontainsid="root"and that the spelling matches the selector.- “Functions are not valid as a React child”: render
<App />, notApp. - Adjacent JSX error: put the siblings inside one parent or a Fragment.
classwarning: JSX usesclassName.- Component not rendering: capitalize both its declaration and its use.
- Broken import: match the filename's casing. A production host may be case-sensitive even when Windows is not.
- JSX expression unexpectedly shows nothing: inspect whether it evaluates to
null,undefined, orfalse.
Do not make a warning disappear by deleting Strict Mode. Read the warning, reduce the problem to the smallest component that still reproduces it, and inspect JSX values with the Components panel or temporary logs placed outside returned JSX.
Accessibility and performance
JSX does not remove the semantics of HTML, so choose elements for their meaning. Keep one clear h1, use ordered heading levels, put list items inside list containers, and preserve landmarks such as main. A Fragment is particularly useful when adding a div would damage list or table semantics. React automatically escapes interpolated text, which helps prevent injection, but that is not a reason to introduce raw HTML without a reviewed need.
Vite's fast development feedback is not a measurement of production performance. Run npm run build to verify bundling, and remove unused starter assets and imports. For a static page, do not split every component or add memoization without a concrete problem to solve.
Practice
Build a simple React page from components. Work through the tiers in order: first make the component structure work, then move a component across a module boundary, and finally use the tools to diagnose deliberate failures.
Tiered exercises
Core: Add Header, TaskPreview, and Footer components. Show your name and two tasks with semantic landmarks.
Stretch: Move Header into its own module and pass an owner prop. Add a JavaScript expression that pluralizes an open count.
Challenge: Deliberately introduce three JSX errors, record the console messages, then correct them. Run both npm run build and npm run preview.
// src/components/Header.jsx
export default function Header({ owner }) {
return (
<header>
<p>Workspace</p>
<h1>{owner}'s Task Manager</h1>
</header>
);
}
// src/App.jsx
import Header from './components/Header.jsx';
const tasks = ['Learn JSX', 'Compose components'];
function TaskPreview() {
return (
<section aria-labelledby="preview-heading">
<h2 id="preview-heading">Preview</h2>
<ul>
<li>{tasks[0]}</li>
<li>{tasks[1]}</li>
</ul>
</section>
);
}
function Footer() {
return <footer><small>Local learning project</small></footer>;
}
export default function App() {
const openCount = tasks.length;
return (
<main>
<Header owner="Suriya" />
<p>{openCount} {openCount === 1 ? 'task' : 'tasks'} open</p>
<TaskPreview />
<Footer />
</main>
);
}
The three example repairs are: change class to className, close <input />, and wrap two returned headings in <>...</>. Verify the repaired project with npm run build, then inspect the production result with npm run preview.
Exit questions
- What problem does this concept solve?
- What is one common mistake?
- Can you explain the code without reading it line by line?
Recap
Vite serves and builds the module graph; React renders the component tree into a root. JSX is JavaScript syntax transformed into element creation, and braces provide access to expressions. Fragments group siblings without adding a DOM node. A function component is a capitalized JavaScript function that returns React nodes.
Official references
- Vite: Getting Started
- React: Writing Markup with JSX
- React: JavaScript in JSX with Curly Braces
- React:
createRoot - React versions
Interview questions
- What does Vite do that React does not?
- Why does
createRootreceive a DOM element whileroot.renderreceives<App />? - Why is Strict Mode useful even though its extra development behavior is not production behavior?
Debug drill: delete the root id, change an import's case, and render App without JSX, one at a time. Read each error, restore the cause, and run npm run build.
2026 depth expansion: setup is part of the runtime model
The setup is part of the runtime model, not disposable boilerplate. For this course, use a modern Vite React project rather than Create React App.
npm create vite@latest react-lab -- --template react
cd react-lab
npm install
npm run dev
A typical browser entry point is:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
const container = document.getElementById('root');
createRoot(container).render(
<StrictMode>
<App />
</StrictMode>,
);
createRoot is for a client-rendered root. hydrateRoot is different: it attaches React behavior to HTML that was already rendered on the server. Do not use “render” and “hydrate” as interchangeable terms. The starting HTML and the work React must perform are different in those two cases.
Modern JSX transform
React 19 requires the modern JSX transform. A normal Vite project configures it for you, so you usually do not need:
import React from 'react';
just to write JSX.
JSX is syntax that becomes JavaScript element creation. This:
<Card title="Inbox">
<TaskCount count={3} />
</Card>
is not HTML and it is not a string. It describes React elements that React will later reconcile with the rendered tree.
Development tooling that belongs in the baseline
Install and use the following as part of a sensible development baseline:
- React DevTools browser extension;
- ESLint with the current React Hooks rules;
- browser Network, Performance, and Accessibility tools;
- production builds when investigating performance.
React's Hooks lint rules are not cosmetic formatting preferences. They encode requirements on purity and dependency usage that React and the React Compiler depend on. Treat violations as signals to investigate rather than warnings to silence.
Project structure
As a project grows, prefer feature boundaries over one giant set of type-based folders:
src/
├─ app/
│ ├─ App.jsx
│ └─ providers.jsx
├─ features/
│ └─ tasks/
│ ├─ TaskList.jsx
│ ├─ TaskForm.jsx
│ ├─ taskApi.js
│ └─ taskQueries.js
├─ shared/
│ ├─ ui/
│ └─ lib/
└─ main.jsx
The exact directory names are less important than dependency direction. Generic shared code should not secretly import one business feature, and ownership of data should remain visible in the structure. A folder layout is useful when it communicates those boundaries rather than merely categorizing files.
Setup debugging checklist
If the screen is blank, move from the browser boundary toward the component:
- inspect the browser console;
- confirm
#rootexists; - confirm the import path and filename casing;
- confirm the component returns JSX;
- inspect the Vite terminal for compile errors;
- verify you rendered
<App />, notApp; - inspect the Elements panel to distinguish “React rendered nothing” from “CSS hid it.”
Treat each tool as evidence. The console can identify a runtime failure, the terminal can identify a transform or import failure, and the Elements panel can show whether React produced DOM that CSS then hid. That is more useful than opening tools as a ritual.
Deep dive: what JSX actually compiles into
JSX is syntax for describing elements. Conceptually:
const element = (
<button className="primary">
Save
</button>
);
becomes element-creation calls handled by the JSX runtime. The exact generated call is an implementation detail of the configured transform; the useful mental model is that the result is a React element description, not a browser DOM node created at that line.
The practical consequence is that values inside {} are JavaScript expressions:
const name = 'Maya';
const unread = 3;
return (
<p>
{name} has {unread} unread messages.
</p>
);
You cannot place arbitrary statements directly in a JSX expression position:
// Invalid idea
<p>{if (ready) { 'Ready' }}</p>
Use an expression instead:
<p>{ready ? 'Ready' : 'Waiting'}</p>
Or perform the statement-based work before the return and render the resulting value:
let message = 'Waiting';
if (ready) {
message = 'Ready';
}
return <p>{message}</p>;
The boundary is worth learning early: JavaScript statements belong in the function body, while the braces in JSX accept expressions that produce a value.
JSX is stricter than HTML
JSX follows JavaScript property naming and requires a complete element tree, so several familiar HTML spellings change:
<label htmlFor="email">Email</label>
<input className="field" />
not:
<label for="email">Email</label>
<input class="field">
Custom CSS properties and data-* / aria-* attributes keep their normal names:
<div
data-state="open"
aria-expanded={open}
style={{
'--panel-gap': '1rem',
}}
/>
Style object
<div
style={{
backgroundColor: 'black',
fontSize: 18,
}}
/>
Here the value of style is a JavaScript object, not a CSS string. The outer braces enter JavaScript from JSX, and the inner braces create the object.
Prefer classes for most reusable styling. Inline style is a reasonable choice when the values are genuinely dynamic, but it should not become the default way to express an application's stylesheet.
Children are ordinary props with special syntax
These two forms are conceptually related:
<Card>
<p>Hello</p>
</Card>
and:
<Card children={<p>Hello</p>} />
The first is the idiomatic composition syntax. JSX places the nested content into the children prop for you.
A component can receive that prop explicitly:
function Card({ children }) {
return <section className="card">{children}</section>;
}
Children can be:
- one element;
- multiple elements;
- strings/numbers;
- conditionally null;
- arrays;
- fragments.
The component decides how to place those children, while the caller decides what content to compose into it.
Fragments
When a component needs to return sibling elements without adding an extra DOM wrapper, use a Fragment:
return (
<>
<dt>{term}</dt>
<dd>{definition}</dd>
</>
);
Fragments affect the React tree but do not create a host DOM element. That distinction matters when the surrounding HTML has strict content rules, such as a dt and dd needing to remain direct children of a description list.
When mapping a list of Fragments, use the long form so that you can supply a key:
import { Fragment } from 'react';
items.map((item) => (
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.definition}</dd>
</Fragment>
));
The shorthand <>...</> cannot receive a key, which is why the long form is needed in this case.
Expressions and object pitfalls
This JSX:
<p>{user}</p>
fails when user is a plain object because React cannot render arbitrary objects as text children.
Render the field the interface actually needs:
<p>{user.name}</p>
For diagnostics, serialize the object explicitly:
<pre>{JSON.stringify(user, null, 2)}</pre>
That serialization is a debugging tool, not a normal product UI. In an actual interface, choose and format the fields the user needs instead of exposing an internal object shape.
JSX and security
React escapes string content placed into JSX children:
<p>{comment.body}</p>
If comment.body contains:
<script>alert(1)</script>
React treats it as text rather than executable markup.
That protection is bypassed when using:
dangerouslySetInnerHTML
Only inject HTML when the product genuinely requires trusted or sanitized HTML. Sanitization requires a security-reviewed HTML sanitizer, not a regular expression. Treat external and network-provided content as untrusted even when the component is rendering it through JSX.
Setup deep dive: development versus production
Vite development mode provides:
- module hot replacement;
- source maps;
- fast transforms;
- development diagnostics.
A production build:
npm run build
changes the assumptions you are making:
- output is minified and bundled;
- development-only Strict Mode behavior is not the same production behavior;
- chunking matters;
- source-map policy matters;
- performance must be measured in this mode.
Never conclude that a React app is slow solely from development mode. Development tooling intentionally adds work and changes the shape of the output.
Environment variables
With Vite, client-exposed values are bundled into browser JavaScript. Anything sent to the browser is public, regardless of whether it came from an environment file.
Do not put any of these into client environment variables:
database passwords
private API keys
JWT signing secrets
service credentials
A prefix convention such as VITE_* is not a security boundary. It only controls which values Vite exposes to the client bundle. Secrets belong on a server-side boundary that the browser cannot inspect.
Debugging JSX errors
"Adjacent JSX elements must be wrapped"
Return one parent or Fragment.
"Objects are not valid as a React child"
Render a primitive field or map the object to UI.
Blank screen after compile success
Check these possibilities in order:
- browser console;
- root container;
- component import/export mismatch;
- runtime exception;
- CSS hiding content;
- route path;
- whether a component returned
undefined.
Compile success only tells you that the source transformed successfully. It does not rule out a runtime exception, a routing mismatch, or DOM that is present but hidden by CSS.
Import mismatch
Named export:
export function Button() {}
requires:
import { Button } from './Button.jsx';
Default export:
export default function Button() {}
requires:
import Button from './Button.jsx';
Do not debug React state when the actual problem is the ES module contract. First verify whether the exporting and importing sides agree about named versus default exports.
Worked exercise: convert static HTML to JSX
Start with this HTML:
<section class="profile">
<label for="bio">Bio</label>
<textarea id="bio"></textarea>
</section>
Convert the HTML to JSX:
function ProfileEditor() {
return (
<section className="profile">
<label htmlFor="bio">Bio</label>
<textarea id="bio" />
</section>
);
}
Then make the editor dynamic:
function ProfileEditor({ profile }) {
const descriptionId = `${profile.id}-description`;
return (
<section className="profile">
<h2>{profile.name}</h2>
<label htmlFor={descriptionId}>Bio</label>
<textarea
id={descriptionId}
defaultValue={profile.bio}
/>
</section>
);
}
The label and textarea remain connected because both use the same generated ID. Later lessons replace this handcrafted ID concatenation with useId for reusable fields.
Mastery check
You should be able to explain why:
- JSX is not HTML;
{}accepts expressions;- Fragment can avoid unnecessary DOM nodes;
- string children are escaped;
- environment variables in the client are public;
- development behavior is not the same as production performance.
