Skip to content

React Components, State, Forms, and Routing

Step 19 of 45

React Components and JSX: Props and Project Structure

Introduction

This lesson opens chapter 5, aligned with course 5 of the Meta Front-End Developer pathway ("React Basics"). Until now you built pages from HTML, CSS and JavaScript that manipulates the DOM directly. That works for small pages, but as an interface grows, keeping the DOM in sync with your data by hand becomes slow and error-prone. React is a JavaScript library that lets you describe what the interface should look like for a given piece of data, and then updates the DOM for you.

React's unit of construction is the component: a reusable function that returns a piece of the interface. In this lesson you learn to write function components, use JSX (the syntax that looks like HTML inside JavaScript), pass data with props, nest components with children, and set up and organize a small standalone React project. The practice scenario is original: a catalog of Harbor Workshops course cards built from one reusable WorkshopCard component.

Learning objectives

  1. Explain what React does and why components help.
  2. Set up a small React project with a current build tool and explain the generated structure.
  3. Write function components with capitalized names and export and import them.
  4. Write valid JSX: one root, expressions in braces, className, self-closing tags, and camelCase attributes.
  5. Pass data with props, use destructuring and default values, and explain one-way data flow.
  6. Use the children prop for composition.
  7. Explain why props are read-only.
  8. Organize files and render a list of components from data.

1. What React is, and what it is not

React is a library for building user interfaces from components. Its core idea is declarative rendering: you write a function that, given data, returns what the screen should show. When a state update or another React render trigger occurs, React computes the output again and commits the necessary DOM changes. Assigning an ordinary JavaScript variable does not notify React.

Compare two ways to show a seat count.

Imperative (manual DOM updates):

JavaScript
const el = document.querySelector('#seats');
el.textContent = 'Seats left: ' + seats;
// and remember to call this every time seats changes, everywhere

Declarative (React):

JSX
function SeatCounter({ seats }) {
  return <p>Seats left: {seats}</p>;
}

You never write "update the paragraph." You describe the paragraph for any value of seats.

React does not bundle an application router or a full data layer. It provides UI and form primitives, while frameworks and libraries supply broader routing and data workflows (routing arrives in lesson 4 of this chapter). Learn the principles; the surrounding tools change faster than React's core ideas.

2. Setting up a project

The React team no longer recommends Create React App for new projects: it is deprecated. For learning, the official guidance for building from scratch is to start with a build tool such as Vite, and for production applications the documentation recommends starting with a framework. In this course you build small standalone apps with React 19 APIs, so Vite is a good fit. Check the generated dependency versions and commit the lock file.

Prerequisites: a supported Node.js release and a terminal (chapter 3). The current Vite guide requires Node.js 20.19+ or 22.12+; some templates require newer versions. Node.js 24 is a suitable supported baseline for this exercise. Read the generated package's engine requirements.

Bash
npm create vite@latest harbor-workshops -- --template react
cd harbor-workshops
npm install
npm run dev

The dev server prints a local address such as http://localhost:5173. Edits you save appear in the browser immediately (hot reload). Template names and prompts change between Vite versions, so read the terminal output and the official guide if something differs.

The generated structure

Text
harbor-workshops/
  index.html          # the single HTML page with <div id="root">
  package.json        # dependencies and scripts (dev, build, preview, lint)
  vite.config.js      # build tool configuration
  public/             # files copied as-is (favicon, robots.txt)
  src/
    main.jsx          # entry point: renders <App /> into #root
    App.jsx           # the top-level component
    App.css
    assets/           # images imported by components

The entry point connects React to the page:

JSX
// src/main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
);

createRoot takes a DOM node and renders the component tree inside it. StrictMode adds development-only checks, including extra render calls and effect/ref cleanup cycles to expose missing cleanup or impure code. It does not deliberately double-call event handlers, and these checks do not run in production.

The package.json scripts: npm run dev starts the dev server, npm run build creates optimized static files in dist/, and npm run preview serves that build locally for testing. Commit package.json and the lock file, but add node_modules and dist to .gitignore (chapter 3).

3. Function components

The function components in this chapter return JSX or null; React also accepts other renderable values such as strings and arrays. Its name must start with a capital letter; lowercase names are treated as HTML tags.

JSX
function Greeting() {
  return <h2>Welcome to Harbor Workshops</h2>;
}

export default function App() {
  return (
    <main>
      <Greeting />
      <Greeting />
    </main>
  );
}

Using <Greeting /> renders the function's result. Each use is an independent instance. Rules:

  • Define components at the top level of a module. Never define a component inside another component, because it would be a new component on every render and lose its state.
  • Keep one component per responsibility, and split when a component gets long or when a section is reused.
  • A component should be pure: given the same props, state, and context, it computes the same output and does not change things that existed before it was called (no modifying outside variables during rendering).

Exports and imports

JSX
// WorkshopCard.jsx
export default function WorkshopCard() { /* ... */ }   // default export
export function Badge() { /* ... */ }                  // named export
JSX
import WorkshopCard, { Badge } from './WorkshopCard.jsx';

A file can have one default export (imported under any name) and many named exports (imported with braces, by exact name). Many teams use one component per file, named like the file, with a default export. Review JavaScript modules and import syntax when organizing files. Relative paths start with ./ or ../; explicit extensions make the examples easier to follow.

4. JSX

JSX is a syntax extension that lets you write markup inside JavaScript. A build step compiles it to plain JavaScript function calls. It looks like HTML but has stricter rules.

Rules

  1. Return a single root element. Wrap siblings in a parent or a fragment <>...</> (a fragment adds no DOM node).
JSX
return (
  <>
    <h2>Pottery</h2>
    <p>Saturday morning</p>
  </>
);
  1. Close every tag. <img />, <input />, <br />.
  2. Use camelCase for most attributes, and className instead of class, htmlFor instead of for: <label htmlFor="email" className="field">.
  3. Put JavaScript expressions in curly braces.
JSX
const title = 'Pottery for Beginners';
const price = 45;
return (
  <article>
    <h3>{title}</h3>
    <p>Base price: {price.toFixed(2)} EUR; illustrative 20% surcharge: {(price * 0.2).toFixed(2)} EUR</p>
    <img src={imageUrl} alt={`Photo for ${title}`} width="640" height="400" />
  </article>
);

Braces accept expressions (variables, function calls, ternaries, template strings), not statements (if, for). Strings in attributes can use quotes (alt="Logo"); anything dynamic uses braces (alt={title}).

  1. Inline styles take an object: <p style={{ color: 'crimson', fontSize: '1.25rem' }}>. The double braces are a JavaScript object inside the JSX expression. Prefer CSS classes for anything non-trivial.
  2. Comments inside JSX are {/* like this */}.

Escaping and safety

React escapes strings rendered as text children, so user text such as <script> is displayed as text. This does not make every URL, HTML injection, or third-party DOM operation safe. This is a protection against cross-site scripting. The prop dangerouslySetInnerHTML bypasses it and should be avoided unless the HTML is trusted and sanitized.

What renders, and what does not

true, false, null, and undefined render nothing. Numbers and strings render as text. Plain data objects are not valid children and throw an error ("Objects are not valid as a React child"). Remember that 0 does render, which surprises people with expressions like {count && <List />} (covered in lesson 3).

5. Props and one-way data flow

Props (properties) are the inputs to a component, passed as attributes and received as a single object argument.

JSX
function WorkshopCard(props) {
  return (
    <article className="card">
      <h3>{props.title}</h3>
      <p>{props.date}</p>
    </article>
  );
}

<WorkshopCard title="Pottery for Beginners" date="Saturday, 10 April 2027" />

Destructuring in the parameter list is the common idiom. Defaults apply to missing or undefined props, not to null or 0:

JSX
function WorkshopCard({ title, date, seatsLeft = 0, featured = false }) {
  return (
    <article className={featured ? 'card card--featured' : 'card'}>
      <h3>{title}</h3>
      <p>{date}</p>
      <p>{seatsLeft > 0 ? `${seatsLeft} ${seatsLeft === 1 ? 'seat' : 'seats'} left` : 'Sold out'}</p>
    </article>
  );
}

Passing non-string values requires braces: seatsLeft={12}, featured={true} (or just featured), tags={['clay', 'beginner']}, onBook={handleBook}.

Props are read-only

A component must never modify its props:

JSX
function Bad({ workshop }) {
  workshop.seatsLeft -= 1; // WRONG: mutates an object owned by the parent
  return <p>{workshop.seatsLeft} seats left</p>;
}

Reassigning a destructured number only changes a local binding; it does not mutate the parent's prop or schedule a render. Changing rendered data should flow through state (next lesson). Use React state and event handlers when a child needs to request a change: the parent passes a callback prop and the child calls it.

One-way data flow

Data flows down the component tree, from parent to child, through props. A child cannot reach up and change the parent's variables. This makes behavior predictable: to find where a value comes from, follow props upward. When two components need the same data, the data lives in their closest common parent and flows down to both ("lifting state up", lesson 2).

Spreading and passing many props

JSX
const workshop = { title: 'Pottery', date: 'Saturday', seatsLeft: 4 };
<WorkshopCard {...workshop} />

Spreading is convenient but hides what a component receives. Use it with care; explicit props are easier to read.

6. Children and composition

Whatever you put between a component's tags arrives as the special prop children.

JSX
function Panel({ title, children }) {
  return (
    <section className="panel">
      <h2>{title}</h2>
      {children}
    </section>
  );
}

<Panel title="Your booking">
  <p>Two seats on Saturday.</p>
  <button type="button">Change</button>
</Panel>

Panel does not know what its content is; it provides the frame. This composition is how React reuses layout: build small generic pieces (Panel, Card, Stack) and combine them. Prefer composition over inventing many boolean props like hasHeader, hasFooter, isCompact.

Components can also receive JSX as other props (<Layout sidebar={<Filters />}>), which gives more than one slot.

7. Rendering lists of components

Data in arrays becomes components with map. Each item needs a stable key (covered fully in lesson 3):

JSX
const workshops = [
  { id: 'pottery', title: 'Pottery for Beginners', date: 'Saturday, 10 April 2027', seatsLeft: 4 },
  { id: 'bread',   title: 'Sourdough Basics',      date: 'Sunday, 11 April 2027',   seatsLeft: 0 },
  { id: 'print',   title: 'Linocut Printing',      date: 'Saturday, 17 April 2027', seatsLeft: 9 }
];

function Catalog() {
  return (
    <ul className="event-grid">
      {workshops.map(w => (
        <li key={w.id}>
          <WorkshopCard title={w.title} date={w.date} seatsLeft={w.seatsLeft} />
        </li>
      ))}
    </ul>
  );
}

Expected output: three list items, with "4 seats left", "Sold out", and "9 seats left". Notice that the layout (the ul and its grid class from chapter 4) lives in Catalog, and the card's internals live in WorkshopCard.

8. Project structure

For a small app, a flat structure is enough:

Text
src/
  main.jsx
  App.jsx
  components/
    WorkshopCard.jsx
    Panel.jsx
    Catalog.jsx
  data/
    workshops.js
  styles/
    index.css

Guidelines:

  • One component per file, file named like the component (WorkshopCard.jsx).
  • Group by feature when the app grows (features/booking/), keeping a feature's components, styles, and tests together.
  • Keep data and logic separate from presentation when it helps readability (a data/ module, helpers in utils/).
  • Import images from src/assets when the bundler should process them; place static files that must keep their names in public/.
  • Import component CSS next to the component, using class names (BEM from chapter 4) to avoid collisions, or use CSS modules (WorkshopCard.module.css), which generate scoped class names. Ordinary CSS imports remain global; BEM reduces naming collisions by convention.
  • Do not over-engineer: create folders when the number of files demands it.

Component names are PascalCase; helper functions and variables are camelCase; event handler props start with on (onBook), and the functions that handle them start with handle (handleBook).

Worked example: a course-card catalog

Brief. The designer gives you three workshops and a card design: title, date, price and seats, with a "Book seats" button that is disabled when sold out. Build a reusable component and render the catalog.

Step 1: create the project. Run the Vite commands above and delete the demo content of App.jsx.

Step 2: model the data. Create the complete src/data/workshops.js module. These are fixed fictional dates and prices for the exercise.

JavaScript
export const workshops = [
  { id: 'pottery', title: 'Pottery for Beginners', date: '10 April 2027', price: 45, seatsLeft: 4 },
  { id: 'bread', title: 'Sourdough Basics', date: '11 April 2027', price: 38, seatsLeft: 0 },
  { id: 'print', title: 'Linocut Printing', date: '17 April 2027', price: 50, seatsLeft: 9 },
];

Step 3: build WorkshopCard. Decide the props from the design: title, date, price, seatsLeft. The button is part of the card.

JSX
// src/components/WorkshopCard.jsx
export default function WorkshopCard({ title, date, price, seatsLeft }) {
  const soldOut = seatsLeft === 0;
  return (
    <article className="card">
      <h3 className="card__title">{title}</h3>
      <p>{date}</p>
      <p>{price.toFixed(2)} EUR</p>
      <p>{soldOut ? 'Sold out' : `${seatsLeft} ${seatsLeft === 1 ? 'seat' : 'seats'} left`}</p>
      <button className="btn" type="button" disabled={soldOut}>
        Book seats
      </button>
    </article>
  );
}

soldOut is a derived value computed from a prop during rendering, with no extra state needed.

Step 4: build Catalog and App. The catalog passes every prop used by the final card, including its numeric price.

JSX
// src/components/Catalog.jsx
import WorkshopCard from './WorkshopCard.jsx';
import { workshops } from '../data/workshops.js';

export default function Catalog() {
  return (
    <section aria-labelledby="catalog-title">
      <h2 id="catalog-title">Workshop catalog</h2>
      <ul className="event-grid">
        {workshops.map(w => (
          <li key={w.id}>
            <WorkshopCard title={w.title} date={w.date}
                          price={w.price} seatsLeft={w.seatsLeft} />
          </li>
        ))}
      </ul>
    </section>
  );
}
JSX
// src/App.jsx
import Catalog from './components/Catalog.jsx';

export default function App() {
  return (
    <main className="container">
      <h1>Harbor Workshops</h1>
      <Catalog />
    </main>
  );
}

Import the chapter 4 grid/card styles if you want the designed layout; JSX class names alone do not create that CSS. The enabled booking buttons are display placeholders in this lesson: event behavior arrives next.

Step 5: run and verify. npm run dev. You should see one h1 and three cards; the second card shows "Sold out" with a disabled button. Open DevTools to confirm the generated DOM is plain semantic HTML, then Tab through the page.

Why this design? WorkshopCard knows nothing about where its data comes from, so you can reuse it in search results, a profile page, or a test. The alternative of copying the markup three times would mean fixing every bug three times. Passing the whole workshop object as one prop is also acceptable, but explicit props make the component's needs visible. Storing soldOut in state would be worse, because it could drift out of sync with seatsLeft.

Common pitfalls

  • Lowercase component names, so <workshopCard /> is treated as an unknown HTML tag.
  • Multiple roots without a wrapper or fragment.
  • class and for instead of className and htmlFor.
  • Statements in braces: {if (x) ...} is invalid; use a ternary or &&.
  • Mutating props or variables that exist outside the component during rendering.
  • Defining a component inside another component.
  • Forgetting braces for non-string props (seatsLeft="4" passes a string).
  • Calling a component like a function (WorkshopCard(props)) instead of using <WorkshopCard />.
  • Missing key when mapping lists.
  • Following out-of-date tutorials that recommend deprecated Create React App for new projects. Class components remain supported, but this course teaches modern function components.

Study tip: Recognize valid JSX quickly: single root, closed tags, className, expressions in braces, capitalized component names. When a question asks how data moves between components, the answer is "down, via props"; children cannot modify props, and shared data lives in the nearest common parent. If a value can be computed from props, it should be computed, not stored.

Mini case study: the card that always says "0 seats left"

Situation. A teammate's card shows "0 seats left" for every workshop, even those with seats, and never shows "Sold out". The code is:

JSX
function WorkshopCard({ title, seatsLeft }) {
  const soldOut = seatsLeft = 0;
  return (
    <article>
      <h3>{title}</h3>
      <p>{soldOut ? 'Sold out' : seatsLeft + ' seats left'}</p>
    </article>
  );
}
// <WorkshopCard title="Sourdough" seatsLeft="12" />

Questions. (1) What are the bugs? (2) What does the screen show? (3) How do you fix it?

Solution.

  1. seatsLeft = 0 is an assignment, not a comparison; it reassigns the local variable without changing the parent's value and evaluates to 0, which is falsy. Also, seatsLeft="12" passes the string "12", not a number.
  2. soldOut is 0, which is falsy, so the ternary takes the second branch, and seatsLeft has just been overwritten with 0. The screen shows 0 seats left for every card while the title stays correct.
  3. Use a comparison and a numeric prop: const soldOut = seatsLeft === 0; and seatsLeft={12}. For a robust component, validate inputs or use TypeScript types or explicit runtime validation at data boundaries. React 19 no longer runs function-component propTypes checks; TypeScript helps during development but does not validate external data at runtime. Check: with 12 the card displays "12 seats left"; with 0 it displays "Sold out".

Key terms

  • Component: a function that returns JSX and can be reused like an HTML tag.
  • JSX: a syntax extension that describes interface markup inside JavaScript.
  • Props: read-only inputs passed from a parent to a child component.
  • Children: the content nested between a component's opening and closing tags.
  • Composition: building interfaces by combining components.
  • One-way data flow: data moves down the tree through props.
  • Fragment: <>...</>, a wrapper that adds no DOM element.
  • Entry point: the file that renders the root component into the page.
  • Pure component: one that returns the same output for the same props and has no side effects while rendering.
  • Derived value: a value computed from props or state instead of stored.

Summary checklist

  • I can explain declarative rendering and why components help.
  • I can create a project with a current build tool and describe its main files.
  • I name components in PascalCase, define them at the top level, and export and import them correctly.
  • I write valid JSX: one root, closed tags, className, and braces for expressions.
  • I pass props, use destructuring and defaults, and never mutate props.
  • I explain one-way data flow and where shared data should live.
  • I use children for composition.
  • I render arrays with map and a stable key.
  • I compute derived values instead of storing them.
  • I keep the project structure simple and consistent.

Official sources