Quick Reference
| Feature | Pattern | Example |
|---------|---------|---------|
| Server Component | Default (no directive) | async function Page() {} |
| Client Component | 'use client' directive | 'use client'; function Counter() {} |
| Server Action | 'use server' directive | async function submit(formData) {} |
| use() hook | Read promises/context | const data = use(promise) |
| Concept | Syntax |
|---------|--------|
| Conditional render | {condition && <Component />} |
| List render | {items.map(item => <Item key={item.id} />)} |
| Props destructure | function Card({ title, children }) {} |
| State update | setItems(prev => [...prev, newItem]) |
When to Use This Skill
Use for React 19 core concepts:
- Learning React 19 new features and syntax
- Deciding between Server and Client Components
- Setting up Server Actions for forms
- Understanding the use() hook for async data
- Component composition and JSX patterns
- Error handling with Suspense and Error Boundaries
For specific topics: hooks → react-hooks-complete, state management → react-state-management, performance → react-performance
React 19 Fundamentals
Overview
React 19 (released December 2024) introduces significant improvements including the React Compiler, Server Components as first-class citizens, new hooks, and enhanced form handling.
JSX and Components
Function Components
// Basic component
function Greeting({ name }: { name: string }) {
return <h1>Hello, {name}!</h1>;
}
// With children
function Card({ children, title }: { children: React.ReactNode; title: string }) {
return (
<div className="card">
<h2>{title}</h2>
{children}
</div>
);
}
// Arrow function component
const Button: React.FC<{ onClick: () => void; children: React.ReactNode }> = ({
onClick,
children,
}) => (
<button onClick={onClick}>{children}</button>
);
JSX Expressions
function ProductCard({ product }: { product: Product }) {
return (
<div className="product">
{/* Conditional rendering */}
{product.onSale && <span className="badge">Sale!</span>}
{/* Ternary */}
{product.inStock ? (
<button>Add to Cart</button>
) : (
<span>Out of Stock</span>
)}
{/* Logical AND (short-circuit) */}
{product.reviews.length > 0 && (
<Reviews reviews={product.reviews} />
)}
{/* Mapping arrays */}
<ul>
{product.features.map((feature, index) => (
<li key={feature.id || index}>{feature.name}</li>
))}
</ul>
{/* Nullish coalescing */}
<span>{product.discount ?? 0}% off</span>
</div>
);
}
Server vs Client Components
Server Components (Default)
// app/posts/page.tsx - Server Component by default
import { db } from '@/lib/db';
async function PostsPage() {
// Direct database access - runs on server only
const posts = await db.posts.findMany({
orderBy: { createdAt: 'desc' },
});
return (
<div>
<h1>Posts</h1>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
export default PostsPage;
Client Components
// components/Counter.tsx
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
</div>
);
}
Composing Server and Client Components
// Server Component
import { db } from '@/lib/db';
import { LikeButton } from './LikeButton'; // Client Component
async function Post({ id }: { id: string }) {
const post = await db.posts.findUnique({ where: { id } });
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
{/* Client component for interactivity */}
<LikeButton postId={id} initialLikes={post.likes} />
</article>
);
}
// LikeButton.tsx - Client Component
'use client';
import { useState, useTransition } from 'react';
import { likePost } from './actions';
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes);
const [isPending, startTransition] = useTransition();
const handleLike = () => {
startTransition(async () => {
const newLikes = await likePost(postId);
setLikes(newLikes);
});
};
return (
<button onClick={handleLike} disabled={isPending}>
{isPending ? 'Liking...' : `Like (${likes})`}
</button>
);
}
Server Actions
Defining Server Actions
// actions.ts
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.posts.create({
data: { title, content },
});
revalidatePath('/posts');
}
export async function likePost(postId: string) {
const post = await db.posts.update({
where: { id: postId },
data: { likes: { increment: 1 } },
});
return post.likes;
}
Using Server Actions in Forms
// Form with Server Action
import { createPost } from './actions';
function CreatePostForm() {
return (
<form action={createPost}>
<input type="text" name="title" placeholder="Title" required />
<textarea name="content" placeholder="Content" required />
<button type="submit">Create Post</button>
</form>
);
}
Using useActionState
'use client';
import { useActionState } from 'react';
import { createPost } from './actions';
function CreatePostForm() {
const [state, formAction, isPending] = useActionState(createPost, {
error: null,
success: false,
});
return (
<form action={formAction}>
<input type="text" name="title" placeholder="Title" required />
<textarea name="content" placeholder="Content" required />
{state.error && <p className="error">{state.error}</p>}
{state.success && <p className="success">Post created!</p>}
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Post'}
</button>
</form>
);
}
The use() Hook
Reading Promises
import { use, Suspense } from 'react';
// Fetch function that returns a promise
async function fetchComments(postId: string) {
const res = await fetch(`/api/posts/${postId}/comments`);
return res.json();
}
// Component that uses the promise
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise);
return (
<ul>
{comments.map((comment) => (
<li key={comment.id}>{comment.text}</li>
))}
</ul>
);
}
// Parent component
function Post({ postId }: { postId: string }) {
const commentsPromise = fetchComments(postId);
return (
<article>
<h1>Post Title</h1>
<Suspense fallback={<p>Loading comments...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</article>
);
}
Reading Context Conditionally
import { use } from 'react';
import { ThemeContext } from './ThemeContext';
function ThemedButton({ showTheme }: { showTheme: boolean }) {
// use() can be called conditionally (unlike useContext)
if (showTheme) {
const theme = use(ThemeContext);
return <button style={{ background: theme.primary }}>Themed</button>;
}
return <button>Default</button>;
}
Props and State
Props Patterns
// Destructuring props
function UserCard({ user, onEdit, className = '' }: UserCardProps) {
return (
<div className={`user-card ${className}`}>
<h3>{user.name}</h3>
<button onClick={() => onEdit(user.id)}>Edit</button>
</div>
);
}
// Spreading props
function Button({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
return <button {...props}>{children}</button>;
}
// Default props with TypeScript
interface CardProps {
title: string;
elevated?: boolean;
padding?: 'sm' | 'md' | 'lg';
}
function Card({ title, elevated = false, padding = 'md' }: CardProps) {
return (
<div
className={`card ${elevated ? 'elevated' : ''} padding-${padding}`}
>
<h2>{title}</h2>
</div>
);
}
State Updates
'use client';
import { useState } from 'react';
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
// Functional update for derived state
const addTodo = (text: string) => {
setTodos((prev) => [
...prev,
{ id: Date.now(), text, completed: false },
]);
};
// Toggle item
const toggleTodo = (id: number) => {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
// Remove item
const removeTodo = (id: number) => {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
};
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => removeTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
);
}
Suspense and Error Boundaries
Suspense for Loading States
import { Suspense } from 'react';
function App() {
return (
<div>
<h1>My App</h1>
<Suspense fallback={<HeaderSkeleton />}>
<Header />
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<MainContent />
</Suspense>
</div>
);
}
Error Boundaries
'use client';
import { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary fallback={<ErrorPage />}>
<MainContent />
</ErrorBoundary>
);
}
React 19 Error Boundary Hook Pattern
'use client';
import { useErrorBoundary } from 'react-error-boundary';
function DataLoader() {
const { showBoundary } = useErrorBoundary();
const loadData = async () => {
try {
const data = await fetchData();
return data;
} catch (error) {
showBoundary(error);
}
};
// ...
}
Fragments and Portals
Fragments
// Short syntax
function List() {
return (
<>
<li>Item 1</li>
<li>Item 2</li>
</>
);
}
// With key (for mapping)
function DefinitionList({ items }: { items: { term: string; definition: string }[] }) {
return (
<dl>
{items.map((item) => (
<Fragment key={item.term}>
<dt>{item.term}</dt>
<dd>{item.definition}</dd>
</Fragment>
))}
</dl>
);
}
Portals
'use client';
import { createPortal } from 'react-dom';
import { useEffect, useState } from 'react';
function Modal({ children, isOpen }: { children: ReactNode; isOpen: boolean }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted || !isOpen) return null;
return createPortal(
<div className="modal-overlay">
<div className="modal-content">{children}</div>
</div>,
document.body
);
}
Best Practices
1. Component Organization
src/
components/
ui/ # Reusable UI components
Button.tsx
Card.tsx
features/ # Feature-specific components
auth/
LoginForm.tsx
SignupForm.tsx
layout/ # Layout components
Header.tsx
Footer.tsx
hooks/ # Custom hooks
useAuth.ts
useLocalStorage.ts
lib/ # Utilities and helpers
utils.ts
api.ts
types/ # TypeScript types
index.ts
2. Naming Conventions
- Components: PascalCase (
UserProfile.tsx) - Hooks: camelCase with
useprefix (useAuth.ts) - Utilities: camelCase (
formatDate.ts) - Types: PascalCase (
User,ApiResponse)
3. Key Prop Usage
// Good - stable unique identifier
{items.map((item) => (
<Item key={item.id} data={item} />
))}
// Avoid - index as key (unless list is static)
{items.map((item, index) => (
<Item key={index} data={item} /> // Can cause issues with reordering
))}
4. Avoid Inline Object/Function Props
// Bad - creates new object/function every render
<Component style={{ color: 'red' }} onClick={() => handleClick(id)} />
// Good - stable references
const style = useMemo(() => ({ color: 'red' }), []);
const handleClickMemo = useCallback(() => handleClick(id), [id]);
<Component style={style} onClick={handleClickMemo} />