FullStack Course LogoFullStack Course
Module: React and Ecosystem
React and Ecosystem·089·13 MIN READ

089: React Setup + JSX

TOPICS COVERED: 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

  1. What is the difference between rendering and committing?
  2. Why should component rendering be pure?
  3. 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 createRoot binding 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:

bash
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:

text
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:

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 with this small component tree:

jsx
const user = 'Suriya';
const openTasks = 2;

function Header() {
  return (
    <header>
      <p className="eyebrow">Workspace</p>
      <h1>{user}&apos;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:

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 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:

  1. Return one root node. Use a semantic wrapper or <>...</> when the siblings do not need a new DOM element.
  2. Close every tag: <img />, <input />, and <li>...</li> are all required forms.
  3. Most DOM property names use camelCase: className, htmlFor, and onClick are examples.
  4. Put JavaScript expressions, not statements, inside {}. {task.title} and {condition ? a : b} work; {if (...)} does not.
  5. Component names begin with a capital letter. Lowercase names refer to built-in DOM elements.
  6. JSX comments use {/* comment */}.
  7. 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:

jsx
export default function TaskHeader({ owner }) {
  const today = new Intl.DateTimeFormat(undefined, {
    dateStyle: 'full',
  }).format(new Date());

  return (
    <header>
      <h1>{owner}&apos;s Task Manager</h1>
      <p>{today}</p>
    </header>
  );
}

Then import it with the exact path and extension:

jsx
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 that index.html contains id="root" and that the spelling matches the selector.
  • “Functions are not valid as a React child”: render <App />, not App.
  • Adjacent JSX error: put the siblings inside one parent or a Fragment.
  • class warning: JSX uses className.
  • 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, or false.

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.

jsx
// src/components/Header.jsx
export default function Header({ owner }) {
  return (
    <header>
      <p>Workspace</p>
      <h1>{owner}&apos;s Task Manager</h1>
    </header>
  );
}
jsx
// 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

  1. What problem does this concept solve?
  2. What is one common mistake?
  3. 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

Interview questions

  1. What does Vite do that React does not?
  2. Why does createRoot receive a DOM element while root.render receives <App />?
  3. 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.

bash
npm create vite@latest react-lab -- --template react
cd react-lab
npm install
npm run dev

A typical browser entry point is:

jsx
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:

jsx
import React from 'react';

just to write JSX.

JSX is syntax that becomes JavaScript element creation. This:

jsx
<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:

text
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:

  1. inspect the browser console;
  2. confirm #root exists;
  3. confirm the import path and filename casing;
  4. confirm the component returns JSX;
  5. inspect the Vite terminal for compile errors;
  6. verify you rendered <App />, not App;
  7. 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:

jsx
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:

jsx
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:

jsx
// Invalid idea
<p>{if (ready) { 'Ready' }}</p>

Use an expression instead:

jsx
<p>{ready ? 'Ready' : 'Waiting'}</p>

Or perform the statement-based work before the return and render the resulting value:

jsx
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:

jsx
<label htmlFor="email">Email</label>
<input className="field" />

not:

html
<label for="email">Email</label>
<input class="field">

Custom CSS properties and data-* / aria-* attributes keep their normal names:

jsx
<div
  data-state="open"
  aria-expanded={open}
  style={{
    '--panel-gap': '1rem',
  }}
/>

Style object

jsx
<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:

jsx
<Card>
  <p>Hello</p>
</Card>

and:

jsx
<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:

jsx
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:

jsx
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:

jsx
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:

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:

jsx
<p>{user.name}</p>

For diagnostics, serialize the object explicitly:

jsx
<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:

jsx
<p>{comment.body}</p>

If comment.body contains:

html
<script>alert(1)</script>

React treats it as text rather than executable markup.

That protection is bypassed when using:

jsx
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:

bash
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:

text
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:

  1. browser console;
  2. root container;
  3. component import/export mismatch;
  4. runtime exception;
  5. CSS hiding content;
  6. route path;
  7. 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:

jsx
export function Button() {}

requires:

jsx
import { Button } from './Button.jsx';

Default export:

jsx
export default function Button() {}

requires:

jsx
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:

html
<section class="profile">
  <label for="bio">Bio</label>
  <textarea id="bio"></textarea>
</section>

Convert the HTML to JSX:

jsx
function ProfileEditor() {
  return (
    <section className="profile">
      <label htmlFor="bio">Bio</label>
      <textarea id="bio" />
    </section>
  );
}

Then make the editor dynamic:

jsx
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.
Reader page: /react/lesson/089/react-setup-jsx