React SME Cookbook
All FAQs
basicscomponentsexamplesreact-fundamentals

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 dev

Your 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-ts for a lightweight Vite + React setup, or npx create-react-app my-app --template typescript for 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-world points to components/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:

  1. Catch bugs before runtime -- TypeScript flags type mismatches, typos, and missing props at compile time so they never reach your users.
  2. Self-documenting code -- Type annotations make component props and function signatures immediately clear to anyone reading the code.
  3. Superior editor support -- You get autocomplete, inline error highlighting, and reliable refactoring tools that plain JavaScript cannot provide.
  4. Safer refactoring at scale -- Renaming a prop or changing a data shape shows you every file that needs updating, across the entire codebase.
  5. 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 if or for) 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 key prop 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, not onclick)
  • Pass a function reference, not a function call -- onClick={handleClick} not onClick={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>
  );
}
  • useState returns 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}
  • onChange fires 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>
  • children is whatever you put between the opening and closing tags of a component
  • The type React.ReactNode accepts strings, numbers, elements, arrays, or null
  • 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 className instead of class (since class is a reserved word in JavaScript)
  • Inline styles use a JavaScript object with camelCase properties (backgroundColor, not background-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>
  );
}
  • useEffect runs side effects -- things that happen outside of rendering (API calls, timers, subscriptions)
  • The dependency array [userId] tells React to re-run the effect only when userId changes
  • 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 use and 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 useState calls
  • The spread operator { ...prev, [field]: value } creates a new object with one field updated
  • keyof FormData ensures 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 async functions that run only on the server -- no JavaScript ships to the browser for them
  • You can await data directly in the component body; no useEffect, 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 -- revalidatePath vs. 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>
  );
}
  • useActionState wraps a Server Action and returns [state, action, isPending] in one call
  • The isPending flag flips to true during submission and back to false when the action resolves -- perfect for disabling buttons and showing spinners
  • The action receives (prevState, formData) and whatever it returns becomes the new state
  • 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>
  );
}
  • useOptimistic gives you a temporary state that updates instantly, then reverts if the action fails
  • The reducer (current) => current + 1 describes 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 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"
  • 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.all choreography -- 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 ReadableStream with Content-Type: text/event-stream -- the SSE protocol format is just lines like data: <payload>\n\n
  • The browser's built-in EventSource auto-reconnects on network drops and delivers each data: line to onmessage
  • Unlike WebSockets, SSE runs over plain HTTP/2, works through most proxies, and needs zero extra infrastructure
  • Always return a cleanup function from useEffect that calls source.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