089: React Setup + JSX
Learning objective
Outcomes
You will create a current Vite React client, trace its entry point, write function components and JSX expressions, use fragments, and apply JSX rules.
I can start the app, explain every starter file, and render an accessible static Task Manager with the current root API.
Prerequisites
Complete 088 and have Node.js and npm available. You need to read JavaScript imports/exports and run commands from a terminal; no global React installation is required.
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
JSX is a JavaScript syntax extension. It resembles HTML, but a build transform converts it to JavaScript that creates React elements. JSX is neither an HTML string nor a DOM node. Curly braces open a window from JSX into JavaScript expressions.
- Vite: Build tool and dev server providing instant HMR for React projects. — Source: Vite: Getting started
- Module: A file with private scope connected via imports/exports. — Source: MDN: JavaScript modules
- Root: The createRoot binding that tells React which DOM node to manage. — Source: React: createRoot
- React element: The plain object description of UI produced by JSX before rendering. — Source: React: Describing the UI
- Fragment: A wrapper grouping children without adding a DOM node (<></>). — Source: React: Fragment
- HMR: Hot Module Replacement — Vite swaps edited modules in place without losing component state. — Source: Vite: Getting started
As of 2026-08-24, official sources show React docs at 19.2 (npm latest react 19.2.8) and Vite 8.2.2. These verified numbers describe the versions checked here, not a requirement to hard-code a patch version. Use the official moving command npm create vite@latest; the generated lockfile records exact installed packages. Vite 8 requires Node 20.19+ or 22.12+ according to its official guide.
Setup
npm create vite@latest task-manager -- --template react
cd task-manager
npm install
npm run dev
Do not use Create React App. For this curriculum Vite provides a small client-only environment. npm run build creates production assets; npm run preview locally checks that build.
Important files:
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 root-level index.html as source and follows its module script to src/main.jsx.
Beginner complete example
Replace src/main.jsx:
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:
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 makes the section's aria-labelledby reference valid. Replace src/index.css:
: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 real #root DOM node. root.render receives JSX (<App />), not the App function itself. Strict Mode helps reveal impure rendering and effect cleanup bugs in development.
JSX rules
- Return one root node. Use a semantic wrapper or
<>...</>. - Close every tag:
<img />,<input />, and<li>...</li>. - Most DOM names use camelCase:
className,htmlFor,onClick. - Put JavaScript expressions, not statements, in
{}.{task.title}and{condition ? a : b}work;{if (...)}does not. - Components use capitalized names; lowercase names mean built-in DOM elements.
- JSX comments use
{/* comment */}. - Style objects use JavaScript names and values:
style={{ backgroundColor: '#fff' }}.
Curly braces can contain variables, function calls, arithmetic, property access, arrays of nodes, and conditional expressions. Objects cannot be directly rendered as children. Booleans, null, and undefined render nothing.
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 an exact path and extension:
import TaskHeader from './components/TaskHeader.jsx';
export default function App() {
return (
<main>
<TaskHeader owner="Suriya" />
<h2>Today</h2>
</main>
);
}
Keep related small components in one file initially. Split a file when navigation or reuse improves, not because components must each have a file.
Optional advanced: JSX transform and root details
Modern JSX setups do not require import React from 'react' solely for JSX. Hooks and named APIs still need imports. Vite's React plugin transforms JSX and supports development refresh. A client-rendered application usually has one root. Server-rendered markup would use hydrateRoot, but this Vite client starts from an empty root and correctly uses createRoot.
Mistakes and debugging
- Blank page: read the terminal and browser console; a syntax or import error often stops rendering.
Target container is not a DOM element: verifyindex.htmlhasid="root"and spelling matches.- “Functions are not valid as a React child”: render
<App />, notApp. - Adjacent JSX error: wrap siblings in one parent or Fragment.
classwarning: useclassName.- Component not rendering: capitalize its declaration and use.
- Broken import: match filename casing; production hosts may be case-sensitive even if Windows is not.
- JSX expression unexpectedly shows nothing: check whether it evaluates to
null,undefined, orfalse.
Do not fix warnings by deleting Strict Mode. Read the warning, reduce to the smallest component, and verify JSX values with the Components panel or temporary logs outside returned JSX.
Accessibility and performance
JSX preserves HTML semantics, so choose elements by meaning. Maintain one clear h1, ordered heading levels, list containers around list items, and landmarks such as main. A Fragment is useful when an added div would damage list or table semantics. Escape behavior is automatic for interpolated text, which helps prevent injection; do not introduce raw HTML without a reviewed need.
Vite's development speed is not production performance. Run npm run build to verify bundling. Remove unused starter assets and imports. Do not split every component or add memoization for a static page.
Practice
Build a simple React page from components.
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>
);
}
Example repairs: change class to className; close <input />; wrap two returned headings in <>...</>. Verify with npm run build, then 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 transformed JavaScript syntax, expressions enter through braces, and Fragments group siblings without extra DOM. Function components are capitalized JavaScript functions returning 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 the error, restore the cause, and run npm run build.
2026 depth expansion: setup is part of the runtime model
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 the terms “render” and “hydrate” interchangeably.
Modern JSX transform
React 19 requires the modern JSX transform. In a normal Vite project this is already configured. You usually do not need:
import React from 'react';
just to write JSX.
JSX itself 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.
Development tooling that belongs in the baseline
Install and use:
- 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. They encode requirements React and React Compiler depend on, including purity and correct dependency usage.
Project structure
Prefer feature boundaries over giant type-based folders once the project grows:
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 data ownership should remain visible.
Setup debugging checklist
If the screen is blank:
- 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 tooling as evidence, not ceremony.
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 important 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 inside JSX expression positions:
// Invalid idea
<p>{if (ready) { 'Ready' }}</p>
Use an expression:
<p>{ready ? 'Ready' : 'Waiting'}</p>
or compute before the return:
let message = 'Waiting';
if (ready) {
message = 'Ready';
}
return <p>{message}</p>;
JSX is stricter than HTML
Common differences:
<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,
}}
/>
is a JavaScript object, not a CSS string.
Prefer classes for most reusable styling. Inline style is useful when values are genuinely dynamic.
Children are ordinary props with special syntax
These are conceptually related:
<Card>
<p>Hello</p>
</Card>
and:
<Card children={<p>Hello</p>} />
The first is the idiomatic composition syntax.
A component can receive:
function Card({ children }) {
return <section className="card">{children}</section>;
}
Children can be:
- one element;
- multiple elements;
- strings/numbers;
- conditionally null;
- arrays;
- fragments.
Fragments
When a component must return sibling elements without an extra DOM wrapper:
return (
<>
<dt>{term}</dt>
<dd>{definition}</dd>
</>
);
Fragments affect the React tree but do not create a host DOM element.
For lists of fragments, use the long form so a key can be supplied:
import { Fragment } from 'react';
items.map((item) => (
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.definition}</dd>
</Fragment>
));
Expressions and object pitfalls
This JSX:
<p>{user}</p>
fails if user is a plain object because React cannot render arbitrary objects as text children.
Render a field:
<p>{user.name}</p>
For diagnostics:
<pre>{JSON.stringify(user, null, 2)}</pre>
should be a debugging tool, not normal product UI.
JSX and security
React escapes string content:
<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 use HTML injection when the product truly requires trusted/sanitized HTML. Sanitization needs a security-reviewed HTML sanitizer, not a regex.
Setup deep dive: development versus production
Vite development mode gives:
- module hot replacement;
- source maps;
- fast transforms;
- development diagnostics.
A production build:
npm run build
changes assumptions:
- minified/bundled output;
- no development Strict Mode behavior from dev-only checks;
- chunking matters;
- source-map policy matters;
- performance must be measured here.
Never conclude that a React app is slow solely from development mode.
Environment variables
With Vite, client-exposed values are bundled into browser JavaScript.
Anything sent to the browser is public.
Do not put:
database passwords private API keys JWT signing secrets service credentials
into client environment variables.
A prefix convention such as VITE_* is not a security boundary; it merely controls what Vite exposes.
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:
- browser console;
- root container;
- component import/export mismatch;
- runtime exception;
- CSS hiding content;
- route path;
- whether a component returned
undefined.
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 problem is an ES module contract.
Worked exercise: convert static HTML to JSX
Start:
<section class="profile">
<label for="bio">Bio</label>
<textarea id="bio"></textarea>
</section>
Convert:
function ProfileEditor() {
return (
<section className="profile">
<label htmlFor="bio">Bio</label>
<textarea id="bio" />
</section>
);
}
Then make it 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>
);
}
Later lessons replace 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.
