React Basics
23 component examples to get you started with React -- 10 basic and 13 intermediate.
Search across all documentation pages
23 component examples to get you started with React -- 10 basic and 13 intermediate.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Get a new React project running in under a minute:
# Create a new Next.js app (recommended)
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app
npm run devYour app is now running at http://localhost:3000. Edit app/page.tsx to start building. If port 3000 is already in use, Next.js will automatically increment to the next available port (3001, 3002, etc.) -- check your terminal output for the actual URL.
Other options: You can also use
npm create vite@latest my-app -- --template react-tsfor a lightweight Vite + React setup, ornpx create-react-app my-app --template typescriptfor the classic CRA approach (no longer recommended for new projects).
The simplest React component -- a function that returns JSX.
// components/hello-world.tsx
function HelloWorld() {
return <h1>Hello, World!</h1>;
}
export default HelloWorld;To use this component, import it into another file:
// app/page.tsx
import HelloWorld from "@/components/hello-world";
export default function Home() {
return (
<main>
<HelloWorld />
</main>
);
}@/ is a shortcut for your project root -- so @/components/hello-world points to components/hello-world.tsx<HelloWorld />Related: Components -- composition patterns, props, children | Function Component Syntax -- declaration vs. arrow, default vs. named export | JSX and TSX -- the syntax components return
All examples in this guide use TypeScript (.tsx files) because it is the industry standard for professional React development. Here's why:
Use curly braces {} to embed JavaScript expressions inside JSX.
function Greeting() {
const name = "Alice";
return <p>Welcome, {name}!</p>;
}{} create an expression slot -- you can put any JavaScript expression insideif or for) directly inside curly bracesRelated: JSX and TSX -- expression rules, fragments, and JSX compilation | TypeScript + React Basics -- typing variables and expressions
Map over an array to produce multiple elements.
function FruitList() {
const fruits = ["Apple", "Banana", "Cherry"];
return (
<ul>
{fruits.map((fruit) => (
<li key={fruit}>{fruit}</li>
))}
</ul>
);
}.map() transforms each item in the array into a JSX elementkey prop so React can track which items changedRelated: Lists and Keys -- key strategies, nested lists, filtering, and common pitfalls
Props let you pass data from a parent component to a child.
function UserCard({ name, age }: { name: string; age: number }) {
return (
<div>
<h2>{name}</h2>
<p>Age: {age}</p>
</div>
);
}
// Usage: <UserCard name="Bob" age={25} />{ name, age } directly in the parameter list for clean accessRelated: Components -- composition patterns and the children prop | Typing Props -- interfaces, optional props, discriminated unions
Show or hide content based on a condition.
function LoginStatus({ isLoggedIn }: { isLoggedIn: boolean }) {
return (
<div>
{isLoggedIn ? <p>Welcome back!</p> : <p>Please log in.</p>}
</div>
);
}? : is the most common way to conditionally render in JSX&& for "show or nothing": {isLoggedIn && <p>Welcome!</p>}Related: Conditional Rendering -- ternary,
&&, early return, and guard clauses in depth
Attach an event handler to respond to user actions.
function ClickCounter() {
const handleClick = () => {
alert("Button clicked!");
};
return <button onClick={handleClick}>Click me</button>;
}onClick, not onclick)onClick={handleClick} not onClick={handleClick()}Related: Events -- synthetic events, event delegation, and handler patterns | Mouse Events -- click, double-click, hover | Typing Events -- event handler types
Add interactive state to a component.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}useState returns a pair: the current value and a setter functionuseState(0) is the initial value, used only on first renderRelated: useState -- updater functions, lazy initialization, and state batching | Typing State -- typing complex state shapes
Capture what the user types with a controlled input.
import { useState } from "react";
function NameInput() {
const [name, setName] = useState("");
return (
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Your name"
/>
<p>Hello, {name || "stranger"}!</p>
</div>
);
}value={name}onChange fires on every keystroke and updates state with the new valueRelated: Forms -- controlled vs. uncontrolled inputs, form submission | Controlled vs. Uncontrolled -- when to use each | Form Events -- onChange, onSubmit, onBlur
Use the special children prop to wrap content.
function Card({ children }: { children: React.ReactNode }) {
return <div className="border rounded p-4 shadow">{children}</div>;
}
// Usage:
// <Card>
// <h2>Title</h2>
// <p>Some content here.</p>
// </Card>children is whatever you put between the opening and closing tags of a componentReact.ReactNode accepts strings, numbers, elements, arrays, or nullRelated: Composition -- slots, compound components, and layout patterns | Components -- the children prop in depth
Add CSS classes or inline styles to elements.
function StyledBox() {
return (
<div
className="container"
style={{ backgroundColor: "lightblue", padding: "1rem" }}
>
<p>Styled with both className and inline styles.</p>
</div>
);
}className instead of class (since class is a reserved word in JavaScript)backgroundColor, not background-color)Related: Tailwind Setup -- configuring Tailwind in a Next.js project | Tailwind Utilities -- spacing, typography, and layout classes | Dark Mode -- theme switching with Tailwind
Load data from an API when the component mounts.
import { useState, useEffect } from "react";
interface User {
id: number;
name: string;
email: string;
}
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then((res) => res.json())
.then((data) => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <p>Loading...</p>;
if (!user) return <p>User not found.</p>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}useEffect runs side effects -- things that happen outside of rendering (API calls, timers, subscriptions)[userId] tells React to re-run the effect only when userId changes[] means "run once on mount" -- omitting it entirely means "run after every render"Related: useEffect -- cleanup, dependency arrays, and common mistakes | SWR Basic Fetching -- a better approach to data fetching with caching and revalidation | Suspense -- loading states with React Suspense
Share state between sibling components by moving it to their parent.
import { useState } from "react";
function TemperatureInput({
label,
value,
onChange,
}: {
label: string;
value: string;
onChange: (val: string) => void;
}) {
return (
<label>
{label}:{" "}
<input value={value} onChange={(e) => onChange(e.target.value)} />
</label>
);
}
function TemperatureConverter() {
const [celsius, setCelsius] = useState("");
const fahrenheit = celsius ? String((parseFloat(celsius) * 9) / 5 + 32) : "";
return (
<div>
<TemperatureInput label="Celsius" value={celsius} onChange={setCelsius} />
<p>Fahrenheit: {fahrenheit || "--"}</p>
</div>
);
}Related: Context Patterns -- sharing state without prop drilling | Zustand Setup -- when lifting state isn't enough, use a store | Context vs. Zustand -- choosing between Context and Zustand
Control child component visibility from a parent button using state and props.
import { useState } from "react";
interface DataRowsProps {
showRows: boolean;
data: string[];
}
function DataRows({ showRows, data }: DataRowsProps) {
if (!showRows) return null;
return (
<div className="mt-4 rounded-lg border border-gray-200 bg-white shadow-sm">
{data.map((item, index) => (
<div
key={index}
className="border-b border-gray-100 px-4 py-3 last:border-0 hover:bg-gray-50"
>
{item}
</div>
))}
</div>
);
}
function Dashboard() {
const [showData, setShowData] = useState(false);
const items = ["Row 1: User Data", "Row 2: Analytics", "Row 3: Reports"];
return (
<div className="p-6">
<button
onClick={() => setShowData(!showData)}
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
{showData ? "Hide" : "Show"} Details
</button>
<DataRows showRows={showData} data={items} />
</div>
);
}showData) and passes it down as a propshowRows prop -- returning null hides it completelypx-4 py-2), colors (bg-blue-600 text-white), borders (rounded-md border), and interactive states (hover:bg-blue-700)last:border-0 removes the border from the final row for a clean lookRelated: Conditional Rendering -- early return, guards, and visibility patterns | Lifting State Up (Example 12) -- sharing state between siblings | Composition -- building flexible component hierarchies
Build a flexible component using props to control appearance.
function Button({
variant = "primary",
children,
onClick,
}: {
variant?: "primary" | "secondary" | "danger";
children: React.ReactNode;
onClick?: () => void;
}) {
const styles: Record<string, string> = {
primary: "bg-blue-600 text-white",
secondary: "bg-gray-200 text-gray-800",
danger: "bg-red-600 text-white",
};
return (
<button className={`px-4 py-2 rounded ${styles[variant]}`} onClick={onClick}>
{children}
</button>
);
}
// Usage:
// <Button variant="danger" onClick={handleDelete}>Delete</Button>variant = "primary") make props optional with sensible defaults"primary" | "secondary" | "danger" restricts the prop to valid optionsRelated: Compound Components -- multi-part component APIs | Button Component -- a production-ready variant button | Discriminated Unions -- type-safe variant props
Extract reusable logic into a custom hook.
import { useState } from "react";
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = () => setValue((v) => !v);
const setOn = () => setValue(true);
const setOff = () => setValue(false);
return { value, toggle, setOn, setOff };
}
// Usage in a component:
function Accordion({ title, children }: { title: string; children: React.ReactNode }) {
const { value: isOpen, toggle } = useToggle();
return (
<div>
<button onClick={toggle}>
{title} {isOpen ? "▲" : "▼"}
</button>
{isOpen && <div>{children}</div>}
</div>
);
}use and can call other hooksRelated: Custom Hooks Guide -- rules, testing, and patterns for custom hooks | useToggle -- the toggle hook used in this example | Custom Hooks (React Hooks) -- when and why to extract hooks
Manage a form with several inputs using a single state object.
import { useState, type FormEvent } from "react";
interface FormData {
name: string;
email: string;
message: string;
}
function ContactForm() {
const [form, setForm] = useState<FormData>({ name: "", email: "", message: "" });
const handleChange = (field: keyof FormData, value: string) => {
setForm((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
console.log("Submitted:", form);
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="Name"
value={form.name}
onChange={(e) => handleChange("name", e.target.value)}
/>
<input
placeholder="Email"
type="email"
value={form.email}
onChange={(e) => handleChange("email", e.target.value)}
/>
<textarea
placeholder="Message"
value={form.message}
onChange={(e) => handleChange("message", e.target.value)}
/>
<button type="submit">Send</button>
</form>
);
}useState calls{ ...prev, [field]: value } creates a new object with one field updatedkeyof FormData ensures you can only pass valid field names -- typos become compile errorse.preventDefault() on form submit to prevent the default browser page reloadRelated: Form Patterns (Complex) -- multi-step forms and dynamic fields | React Hook Form + Zod -- schema-validated forms at scale | Form Decision Checklist -- choosing the right form strategy | Gherkin Form Decision Checklist -- test-driven form planning
Skip useEffect entirely -- fetch on the server and render ready HTML.
// app/users/page.tsx
interface User {
id: number;
name: string;
email: string;
}
export default async function UsersPage() {
const res = await fetch("https://jsonplaceholder.typicode.com/users", {
next: { revalidate: 60 },
});
const users: User[] = await res.json();
return (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} -- {user.email}
</li>
))}
</ul>
);
}async functions that run only on the server -- no JavaScript ships to the browser for themawait data directly in the component body; no useEffect, no loading flags, no client-side waterfallnext: { revalidate: 60 } option caches the response for 60 seconds, then refetches on the next requestRelated: Server Components -- when to use server vs. client components | Fetching -- caching, revalidation, and parallel fetches | Client Components -- when you need
"use client"
Handle form submissions on the server without writing an API route.
// app/todos/page.tsx
import { revalidatePath } from "next/cache";
async function createTodo(formData: FormData) {
"use server";
const title = formData.get("title") as string;
await db.todo.create({ data: { title } });
revalidatePath("/todos");
}
export default function TodosPage() {
return (
<form action={createTodo}>
<input name="title" placeholder="New todo" required />
<button type="submit">Add</button>
</form>
);
}"use server" directive marks a function as a Server Action -- it only runs on the server, even when called from a client form<form action={...}>; React serializes the form data and invokes the action on submitrevalidatePath("/todos") tells Next.js to purge the cached page so the new todo appears immediatelyfetch, no JSON parsing -- the network call is handled entirely by React and Next.jsRelated: Server Actions -- security, validation, and error handling | Form Actions -- React 19's form action support | Revalidation --
revalidatePathvs.revalidateTag
Track pending state and server responses from a form action.
"use client";
import { useActionState } from "react";
import { subscribe } from "./actions";
interface State {
message: string;
ok: boolean;
}
const initialState: State = { message: "", ok: false };
export function SubscribeForm() {
const [state, formAction, isPending] = useActionState(subscribe, initialState);
return (
<form action={formAction}>
<input name="email" type="email" required />
<button type="submit" disabled={isPending}>
{isPending ? "Subscribing..." : "Subscribe"}
</button>
{state.message && (
<p className={state.ok ? "text-green-600" : "text-red-600"}>{state.message}</p>
)}
</form>
);
}And the matching server action file:
// app/subscribe/actions.ts
"use server";
interface State {
message: string;
ok: boolean;
}
export async function subscribe(_prev: State, formData: FormData): Promise<State> {
const email = formData.get("email");
if (typeof email !== "string" || !email.includes("@")) {
return { message: "Please enter a valid email address.", ok: false };
}
try {
await db.subscriber.create({ data: { email } });
return { message: `Subscribed ${email}!`, ok: true };
} catch (err) {
if (isUniqueConstraintError(err)) {
return { message: "You're already subscribed.", ok: false };
}
return { message: "Something went wrong. Try again.", ok: false };
}
}useActionState wraps a Server Action and returns [state, action, isPending] in one callisPending flag flips to true during submission and back to false when the action resolves -- perfect for disabling buttons and showing spinners(prevState, formData) and whatever it returns becomes the new state"use server" directive at the top of actions.ts marks every export as a Server Action -- they run only on the server, so database calls and secrets stay safeFormData values on the server before trusting them -- the client can submit anything, so typeof email !== "string" and shape checks belong in the action itself"use client"), but the underlying action still runs on the serverRelated: useActionState -- return shape, error handling, and validation patterns | Form Actions -- the React 19 form action contract | Server Actions -- validating FormData on the server
Show the result instantly while the server catches up.
"use client";
import { useOptimistic, useState } from "react";
import { addLike } from "./actions";
export function LikeButton({ postId, initialLikes }: { postId: number; initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes);
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(current) => current + 1,
);
async function handleClick() {
addOptimisticLike(null);
const next = await addLike(postId);
setLikes(next);
}
return (
<button onClick={handleClick}>
♥ {optimisticLikes}
</button>
);
}useOptimistic gives you a temporary state that updates instantly, then reverts if the action fails(current) => current + 1 describes how to apply the optimistic update on top of the real statesetLikes(next)), React reconciles and the optimistic value is replacedRelated: useOptimistic -- reducer patterns, multiple pending updates, and error recovery | Server Actions -- pairing optimistic UI with mutations
Stream parts of the page as their data becomes ready.
// app/dashboard/page.tsx
import { Suspense } from "react";
import { RecentOrders } from "./recent-orders";
import { SalesChart } from "./sales-chart";
export default function DashboardPage() {
return (
<div className="grid grid-cols-2 gap-4">
<Suspense fallback={<p>Loading orders...</p>}>
<RecentOrders />
</Suspense>
<Suspense fallback={<p>Loading chart...</p>}>
<SalesChart />
</Suspense>
</div>
);
}async Server Component that fetches its own data -- the slow one doesn't block the fast one<Suspense fallback={...}> tells React: "show this placeholder until the child resolves"Promise.all choreography -- the tree just renders as each piece is readyRelated: Suspense -- boundaries, nested suspense, and error handling | Streaming -- how Next.js streams HTML | Suspense + Streaming (Performance) -- measuring and optimizing streaming perf | Loading & Error UI -- route-level
loading.tsx
Push live updates from the server to the browser over a single HTTP connection.
// app/api/ticker/route.ts
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
const data = JSON.stringify({ tick: i, time: Date.now() });
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
await new Promise((r) => setTimeout(r, 1000));
}
controller.close();
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}// app/ticker/page.tsx
"use client";
import { useEffect, useState } from "react";
interface Tick {
tick: number;
time: number;
}
export default function TickerPage() {
const [ticks, setTicks] = useState<Tick[]>([]);
useEffect(() => {
const source = new EventSource("/api/ticker");
source.onmessage = (e) => {
setTicks((prev) => [...prev, JSON.parse(e.data)]);
};
return () => source.close();
}, []);
return (
<ul>
{ticks.map((t) => (
<li key={t.tick}>Tick {t.tick} at {new Date(t.time).toLocaleTimeString()}</li>
))}
</ul>
);
}ReadableStream with Content-Type: text/event-stream -- the SSE protocol format is just lines like data: <payload>\n\nEventSource auto-reconnects on network drops and delivers each data: line to onmessageuseEffect that calls source.close() -- otherwise the connection leaks on unmount or navigationRelated: Streaming -- HTML streaming vs. data streaming | Server Actions -- when to mutate vs. subscribe | useEffect -- cleanup functions and subscription patterns
Pass data through the tree without prop drilling.
"use client";
import { createContext, useContext, useState, type ReactNode } from "react";
type Theme = "light" | "dark";
interface ThemeContextValue {
theme: Theme;
toggle: () => void;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>("light");
const toggle = () => setTheme((t) => (t === "light" ? "dark" : "light"));
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used inside <ThemeProvider>");
return ctx;
}
// Usage in any nested component:
function ThemeToggle() {
const { theme, toggle } = useTheme();
return <button onClick={toggle}>Current theme: {theme}</button>;
}createContext makes a container for shared values; <ThemeContext.Provider value={...}> wraps the part of the tree that can read ituseContext(ThemeContext) to read the value -- no need to pass props through every intermediate componentuseTheme) and throwing when the provider is missing gives you a clear error instead of a silent nullRelated: useContext -- provider patterns, performance, and common pitfalls | Context Patterns -- splitting state vs. dispatch, compound providers | Context vs. Zustand -- when to reach for a store instead
Reviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥