Part 1: JSX and components
React is a library for building UIs out of components: self-contained pieces of markup, logic, and styles that compose into larger interfaces. This part gets a project running and covers the three primitives you use every day: JSX, components, and props.
Scaffold with Vite
Vite is the standard build tool for React in 2025. It starts in under a second and has a sensible default config.
npm create vite@latest my-app -- --template react-tscd my-appnpm installnpm run devOpen http://localhost:5173. You should see the Vite + React splash page.
The --template react-ts flag gives you TypeScript from the start. If you genuinely want plain JavaScript, use --template react, but TypeScript is worth the minor extra syntax: it catches prop shape mismatches before the browser does.
What got generated
my-app/├── index.html # single HTML file, Vite injects the bundle here├── vite.config.ts # Vite config, almost never needs touching├── tsconfig.json # TypeScript config├── src/│ ├── main.tsx # entry point, mounts App into #root│ ├── App.tsx # root component│ └── App.css # global styles└── public/ # static assets served as-issrc/main.tsx is the entry point:
import { StrictMode } from 'react'import { createRoot } from 'react-dom/client'import App from './App.tsx'
createRoot(document.getElementById('root')!).render( <StrictMode> <App /> </StrictMode>,)StrictMode runs each component twice in development to surface side-effect bugs. Leave it on.
JSX
JSX looks like HTML inside JavaScript, but it compiles to plain function calls. Write JSX in .tsx (TypeScript) or .jsx (JavaScript) files.
// This JSX:const element = <h1 className="title">Hello, world</h1>
// Compiles to:const element = React.createElement('h1', { className: 'title' }, 'Hello, world')You never call React.createElement directly, but knowing it exists explains a few JSX rules.
JSX rules
One root element. JSX expressions must have a single root. Use a Fragment when you don’t want an extra DOM node.
// Bad: two siblings with no wrapperreturn ( <h1>Title</h1> <p>Paragraph</p>)
// Good: Fragment wrapper (no DOM output)return ( <> <h1>Title</h1> <p>Paragraph</p> </>)className, not class. class is a reserved keyword in JavaScript.
<div className="container">...</div>Self-close empty tags.
<img src="logo.png" alt="logo" /><input type="text" /><br />Expressions in curly braces. Any JavaScript expression goes inside {}.
const name = 'Weylin'const count = 42
return ( <div> <p>Hello, {name}</p> <p>Count: {count * 2}</p> <p>Today: {new Date().toLocaleDateString()}</p> </div>)Conditionals. JSX has no if block, use ternary or &&.
const isLoggedIn = true
return ( <div> {isLoggedIn ? <p>Welcome back</p> : <p>Please log in</p>} {isLoggedIn && <button>Log out</button>} </div>)Gotcha: 0 && <Component /> renders 0, not nothing. Use count > 0 && <Component /> when the value might be falsy-but-not-false.
Functional components
A React component is a function that returns JSX. That’s it.
function Greeting() { return <h1>Hello, world</h1>}Component names must start with a capital letter. Lowercase names (greeting) are treated as native HTML tags and won’t work as components.
Using a component
import Greeting from './Greeting'
function App() { return ( <div> <Greeting /> <Greeting /> </div> )}Each <Greeting /> renders independently.
Props
Props are the inputs to a component, passed as attributes in JSX and received as an object in the function.
interface GreetingProps { name: string role?: string // optional prop}
function Greeting({ name, role = 'visitor' }: GreetingProps) { return ( <p> Hello, {name}! You are a {role}. </p> )}
export default Greeting<Greeting name="Weylin" role="admin" /><Greeting name="Jane" /> // role defaults to 'visitor'TypeScript’s interface on props catches mismatches at compile time: passing the wrong type or a missing required prop is a build error, not a runtime surprise.
Passing different prop types
interface CardProps { title: string count: number isActive: boolean onClick: () => void children: React.ReactNode // anything renderable}
function Card({ title, count, isActive, onClick, children }: CardProps) { return ( <div className={isActive ? 'card card--active' : 'card'} onClick={onClick} > <h2>{title}</h2> <span>{count}</span> {children} </div> )}<Card title="Tasks" count={5} isActive={true} onClick={() => console.log('clicked')}> <p>Slot content goes here</p></Card>children is how you nest content inside a component, the same way <div> wraps content.
Rendering lists
Map over an array and return JSX for each item. Every item needs a key prop that is unique among siblings.
interface Task { id: number title: string done: boolean}
const tasks: Task[] = [ { id: 1, title: 'Learn JSX', done: true }, { id: 2, title: 'Build a component', done: false }, { id: 3, title: 'Pass some props', done: false },]
function TaskList() { return ( <ul> {tasks.map((task) => ( <li key={task.id} style={{ textDecoration: task.done ? 'line-through' : 'none' }} > {task.title} </li> ))} </ul> )}key helps React reconcile which items changed, added, or removed when the list updates. Use stable IDs (database IDs, slugs), not array indexes. Indexes cause subtle bugs when items reorder.
Composing components
Break UIs into small components and compose them. A TaskList can use a TaskItem component per row:
interface TaskItemProps { title: string done: boolean}
function TaskItem({ title, done }: TaskItemProps) { return ( <li style={{ textDecoration: done ? 'line-through' : 'none' }}> {title} </li> )}
export default TaskItemimport TaskItem from './TaskItem'
function TaskList({ tasks }: { tasks: Task[] }) { return ( <ul> {tasks.map((task) => ( <TaskItem key={task.id} title={task.title} done={task.done} /> ))} </ul> )}This is the React way: prefer many small components over few large ones. Small components are easier to test, easier to reuse, and easier to reason about.
Gotchas at this stage
- Forgot to export.
export default ComponentNameat the bottom of the file (orexport function ComponentName). A missing export gives you an empty module error. - JSX must be in scope. With Vite’s React preset, the JSX transform is automatic. You don’t need
import React from 'react'at the top of every file anymore. keyon the wrong element. Thekeyprop goes on the outermost element returned from.map(), not on a child inside it.- Props are read-only. Never mutate a prop directly. If you need to change it, lift state up (Part 2) or use a callback prop.
undefinedrenders nothing,nullrenders nothing,falserenders nothing. But0renders"0". This surprises people when they writearray.length && <Component />.
What’s next
Part 2 covers useState and event handlers: how to make components respond to user input and store changing values.