React Basics
21 component examples to get you started with React -- 10 basic and 11 intermediate.
Quick Install
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).
Basic Examples
1. Hello World
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>
);
}- A React component is just a JavaScript function that returns JSX
- The function name must start with a capital letter (React uses this to distinguish components from HTML tags)
- JSX looks like HTML but gets compiled to JavaScript under the hood
- You export the component from its file, then import it wherever you want to use it
@/is a shortcut for your project root -- so@/components/hello-worldpoints tocomponents/hello-world.tsx- You use the component like an HTML tag:
<HelloWorld />
Related: Components -- composition patterns, props, children | Function Component Syntax -- declaration vs. arrow, default vs. named export | JSX and TSX -- the syntax components return
Why TypeScript?
All examples in this guide use TypeScript (.tsx files) because it is the industry standard for professional React development. Here's why:
- Catch bugs before runtime -- TypeScript flags type mismatches, typos, and missing props at compile time so they never reach your users.
- Self-documenting code -- Type annotations make component props and function signatures immediately clear to anyone reading the code.
- Superior editor support -- You get autocomplete, inline error highlighting, and reliable refactoring tools that plain JavaScript cannot provide.
- Safer refactoring at scale -- Renaming a prop or changing a data shape shows you every file that needs updating, across the entire codebase.
- Industry expectation -- Nearly all professional React codebases and job postings require TypeScript; learning it from day one avoids relearning later.
2. Displaying a Variable
Use curly braces {} to embed JavaScript expressions inside JSX.
function Greeting() {
const name = "Alice";
return <p>Welcome, {name}!</p>;
}- Curly braces
{}create an expression slot -- you can put any JavaScript expression inside - This works for strings, numbers, and any expression that produces a value
- You cannot put statements (like
iforfor) directly inside curly braces
Related: JSX and TSX -- expression rules, fragments, and JSX compilation | TypeScript + React Basics -- typing variables and expressions
3. Rendering a List
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 element- Every list item needs a unique
keyprop so React can track which items changed - Keys should be stable identifiers -- array indexes work but IDs from data are better
Related: Lists and Keys -- key strategies, nested lists, filtering, and common pitfalls
4. Receiving Props
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} />- Props are the inputs to your component -- think of them like function arguments
- We destructure
{ name, age }directly in the parameter list for clean access - Props are read-only -- a component should never modify its own props
Related: Components -- composition patterns and the children prop | Typing Props -- interfaces, optional props, discriminated unions
5. Conditional Rendering
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>
);
}- The ternary operator
? :is the most common way to conditionally render in JSX - You can also use
&&for "show or nothing":{isLoggedIn && <p>Welcome!</p>} - For complex conditions, extract the logic into a variable above the return
Related: Conditional Rendering -- ternary,
&&, early return, and guard clauses in depth
6. Handling a Click Event
Attach an event handler to respond to user actions.
function ClickCounter() {
const handleClick = () => {
alert("Button clicked!");
};
return <button onClick={handleClick}>Click me</button>;
}- Event names in JSX use camelCase (
onClick, notonclick) - Pass a function reference, not a function call --
onClick={handleClick}notonClick={handleClick()} - The handler receives a synthetic event object that works identically across browsers
Related: Events -- synthetic events, event delegation, and handler patterns | Mouse Events -- click, double-click, hover | Typing Events -- event handler types
7. Using useState
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>
);
}useStatereturns a pair: the current value and a setter function- Calling the setter triggers a re-render with the new value
- The argument to
useState(0)is the initial value, used only on first render
Related: useState -- updater functions, lazy initialization, and state batching | Typing State -- typing complex state shapes
8. Text Input
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>
);
}- A controlled input keeps its value in React state --
value={name} onChangefires on every keystroke and updates state with the new value- This gives you full control over the input -- you can validate, transform, or reset it at any time
Related: Forms -- controlled vs. uncontrolled inputs, form submission | Controlled vs. Uncontrolled -- when to use each | Form Events -- onChange, onSubmit, onBlur
9. Rendering Children
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>childrenis whatever you put between the opening and closing tags of a component- The type
React.ReactNodeaccepts strings, numbers, elements, arrays, ornull - This pattern is essential for building layout and wrapper components
Related: Composition -- slots, compound components, and layout patterns | Components -- the children prop in depth
10. Applying Styles
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>
);
}- Use
classNameinstead ofclass(sinceclassis a reserved word in JavaScript) - Inline styles use a JavaScript object with camelCase properties (
backgroundColor, notbackground-color) - In practice, most projects use a CSS framework like Tailwind instead of inline styles
Related: Tailwind Setup -- configuring Tailwind in a Next.js project | Tailwind Utilities -- spacing, typography, and layout classes | Dark Mode -- theme switching with Tailwind
Intermediate Examples
11. Fetching Data with useEffect
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>
);
}useEffectruns side effects -- things that happen outside of rendering (API calls, timers, subscriptions)- The dependency array
[userId]tells React to re-run the effect only whenuserIdchanges - An empty array
[]means "run once on mount" -- omitting it entirely means "run after every render" - Always handle loading and error/empty states so the UI is never blank
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
12. Lifting State Up
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>
);
}- When two components need the same data, move that state to their closest common parent
- The parent passes the value down as a prop and a setter function for the child to call
- This keeps a single source of truth -- the state lives in one place and flows downward
- Derived values (like the Fahrenheit conversion) are computed during render, not stored in separate state
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
13. Reusable Component with Variants
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>- Default parameter values (
variant = "primary") make props optional with sensible defaults - A union type
"primary" | "secondary" | "danger"restricts the prop to valid options - Using a lookup object for styles keeps the component clean and easy to extend
- This pattern scales well -- add new variants by adding one entry to the object
Related: Compound Components -- multi-part component APIs | Button Component -- a production-ready variant button | Discriminated Unions -- type-safe variant props
14. Custom Hook
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>
);
}- Custom hooks are functions that start with
useand can call other hooks - They let you extract and reuse stateful logic without changing your component tree
- The hook encapsulates the state and behavior -- the component just consumes the API
- Returning an object (instead of an array) lets consumers rename destructured values easily
Related: 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
15. Form with Multiple Fields
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>
);
}- Group related inputs into a single state object instead of separate
useStatecalls - The spread operator
{ ...prev, [field]: value }creates a new object with one field updated keyof FormDataensures you can only pass valid field names -- typos become compile errors- Always call
e.preventDefault()on form submit to prevent the default browser page reload
Related: 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
16. Fetching Data in a Server Component
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>
);
}- Server Components are
asyncfunctions that run only on the server -- no JavaScript ships to the browser for them - You can
awaitdata directly in the component body; nouseEffect, no loading flags, no client-side waterfall - The
next: { revalidate: 60 }option caches the response for 60 seconds, then refetches on the next request - Data never passes through the client, so API keys and database credentials stay safely server-side
Related: Server Components -- when to use server vs. client components | Fetching -- caching, revalidation, and parallel fetches | Client Components -- when you need
"use client"
17. Mutating Data with a Server Action
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>
);
}- The
"use server"directive marks a function as a Server Action -- it only runs on the server, even when called from a client form - Pass the function directly to
<form action={...}>; React serializes the form data and invokes the action on submit revalidatePath("/todos")tells Next.js to purge the cached page so the new todo appears immediately- No API route, no
fetch, no JSON parsing -- the network call is handled entirely by React and Next.js
Related: Server Actions -- security, validation, and error handling | Form Actions -- React 19's form action support | Revalidation --
revalidatePathvs.revalidateTag
18. Form State with useActionState
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>
);
}useActionStatewraps a Server Action and returns[state, action, isPending]in one call- The
isPendingflag flips totrueduring submission and back tofalsewhen the action resolves -- perfect for disabling buttons and showing spinners - The action receives
(prevState, formData)and whatever it returns becomes the newstate - Works only inside Client Components (
"use client"), but the underlying action still runs on the server
Related: useActionState -- return shape, error handling, and validation patterns | Form Actions -- the React 19 form action contract | Server Actions -- validating FormData on the server
19. Optimistic UI with useOptimistic
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>
);
}useOptimisticgives you a temporary state that updates instantly, then reverts if the action fails- The reducer
(current) => current + 1describes how to apply the optimistic update on top of the real state - When the real server response comes back (
setLikes(next)), React reconciles and the optimistic value is replaced - If the action throws, the optimistic value automatically rolls back -- no manual cleanup
Related: useOptimistic -- reducer patterns, multiple pending updates, and error recovery | Server Actions -- pairing optimistic UI with mutations
20. Streaming with Suspense
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>
);
}- Each child is an
asyncServer 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"- Next.js streams the HTML in chunks, so users see content appear progressively instead of staring at a blank page
- No loading state machines, no
Promise.allchoreography -- the tree just renders as each piece is ready
Related: 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
21. Streaming Events with Server-Sent Events
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>
);
}- A Route Handler returns a
ReadableStreamwithContent-Type: text/event-stream-- the SSE protocol format is just lines likedata: <payload>\n\n - The browser's built-in
EventSourceauto-reconnects on network drops and delivers eachdata:line toonmessage - Unlike WebSockets, SSE runs over plain HTTP/2, works through most proxies, and needs zero extra infrastructure
- Always return a cleanup function from
useEffectthat callssource.close()-- otherwise the connection leaks on unmount or navigation
Related: Streaming -- HTML streaming vs. data streaming | Server Actions -- when to mutate vs. subscribe | useEffect -- cleanup functions and subscription patterns