[ PROMPT_NODE_25277 ]
React 19 Patterns
[ SKILL_DOCUMENTATION ]
# React 19 TypeScript Patterns
React 19 introduces breaking changes and new APIs requiring updated TypeScript patterns.
## ref as Prop (No More forwardRef)
React 19 allows ref as regular prop — forwardRef deprecated but still works.
```typescript
// ✅ React 19 - ref as prop
type InputProps = {
ref?: React.Ref;
label: string;
} & React.ComponentPropsWithoutRef;
export function Input({ ref, label, ...props }: InputProps) {
return (
);
}
// Usage
function Form() {
const inputRef = useRef(null);
return (
);
}
```
```typescript
// ❌ Old pattern (still works, but unnecessary)
import { forwardRef } from 'react';
type InputProps = {
label: string;
} & React.ComponentPropsWithoutRef;
export const Input = forwardRef(
({ label, ...props }, ref) => {
return (
);
}
);
Input.displayName = 'Input';
```
### Generic Components with ref
```typescript
type SelectProps = {
ref?: React.Ref;
options: T[];
value: T;
onChange: (value: T) => void;
getLabel: (option: T) => string;
};
export function Select({ ref, options, value, onChange, getLabel }: SelectProps) {
return (
{
const selected = options.find((opt) => getLabel(opt) === e.target.value);
if (selected) onChange(selected);
}}
>
{options.map((opt) => (
{getLabel(opt)}
))}
);
}
```
### Combining ref with Other Props
```typescript
type ButtonProps = {
ref?: React.Ref;
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
} & React.ComponentPropsWithoutRef;
export function Button({
ref,
variant = 'primary',
size = 'md',
className,
children,
...props
}: ButtonProps) {
return (
);
}
```
## useActionState - Form State Management
Replaces useFormState — manages form submission state with Server Actions.
```typescript
'use client';
import { useActionState } from 'react';
type FormState = {
success?: boolean;
errors?: Record;
message?: string;
};
type FormData = {
email: string;
password: string;
};
// Server Action
async function login(
prevState: FormState,
formData: FormData
): Promise {
'use server';
const email = formData.get('email');
const password = formData.get('password');
if (!email || typeof email !== 'string') {
return {
success: false,
errors: { email: ['Email is required'] },
};
}
if (!password || typeof password !== 'string') {
return {
success: false,
errors: { password: ['Password is required'] },
};
}
try {
await signIn(email, password);
return { success: true, message: 'Logged in successfully' };
} catch (error) {
return {
success: false,
message: 'Invalid credentials',
};
}
}
// Client Component
export function LoginForm() {
const [state, formAction, isPending] = useActionState(
login,
{} // Initial state
);
return (
);
}
```
### Conditional use()
use() can be called conditionally — unlike hooks.
```typescript
'use client';
import { use } from 'react';
type Props = {
userPromise?: Promise;
userId?: string;
};
export function UserDisplay({ userPromise, userId }: Props) {
let user: User | undefined;
if (userPromise) {
user = use(userPromise); // Conditional use() - allowed!
} else if (userId) {
// Fetch inline
user = use(fetchUser(userId));
}
if (!user) return
);
}
```
### useOptimistic with State Updates
```typescript
'use client';
import { useOptimistic, useState, useTransition } from 'react';
type Item = { id: string; name: string; quantity: number };
export function ShoppingCart({ items: initialItems }: { items: Item[] }) {
const [items, setItems] = useState(initialItems);
const [isPending, startTransition] = useTransition();
const [optimisticItems, updateOptimistic] = useOptimistic(
items,
(state, { id, quantity }: { id: string; quantity: number }) =>
state.map((item) =>
item.id === id ? { ...item, quantity } : item
)
);
async function updateQuantity(id: string, quantity: number) {
// Optimistic update
updateOptimistic({ id, quantity });
// Server update
startTransition(async () => {
const updated = await fetch(`/api/cart/${id}`, {
method: 'PATCH',
body: JSON.stringify({ quantity }),
}).then((r) => r.json());
setItems(updated);
});
}
return (
{state.errors?.email?.map((error) => (
{error}
))}
{state.errors?.password?.map((error) => (
{state.message && (
{error}
))}
{state.message}
)}
);
}
```
### useActionState with Optimistic Updates
```typescript
'use client';
import { useActionState, useOptimistic } from 'react';
type Todo = { id: string; title: string; completed: boolean };
async function toggleTodo(
prevState: { todos: Todo[] },
formData: FormData
): Promise {
'use server';
const todoId = formData.get('todoId') as string;
await db.todo.update({
where: { id: todoId },
data: { completed: { toggle: true } },
});
const todos = await db.todo.findMany();
return { todos };
}
export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
const [state, formAction] = useActionState(
toggleTodo,
{ todos: initialTodos }
);
const [optimisticTodos, setOptimisticTodos] = useOptimistic(
state.todos,
(currentTodos, todoId: string) =>
currentTodos.map((todo) =>
todo.id === todoId ? { ...todo, completed: !todo.completed } : todo
)
);
return (
-
{optimisticTodos.map((todo) => (
- { setOptimisticTodos(todo.id); formAction(formData); }} > ))}
{user.name}
{user.email}
No user data
;
return {user.name}
;
}
```
### use() in Loops
```typescript
'use client';
import { use } from 'react';
type Props = {
userPromises: Promise[];
};
export function UserList({ userPromises }: Props) {
return (
-
{userPromises.map((promise, index) => {
const user = use(promise); // use() in loop - allowed!
return
- {user.name} ; })}
Content
;
}
```
## useOptimistic - Optimistic UI Updates
Show immediate UI feedback before server confirms.
```typescript
'use client';
import { useOptimistic } from 'react';
type Message = { id: string; text: string; sending?: boolean };
export function MessageThread({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage: Message) => [...state, newMessage]
);
async function sendMessage(formData: FormData) {
const text = formData.get('message') as string;
// Add optimistic message immediately
addOptimisticMessage({ id: 'temp', text, sending: true });
// Send to server
await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify({ text }),
});
}
return (
-
{optimisticMessages.map((msg) => (
- {msg.text} ))}
-
{optimisticItems.map((item) => (
- {item.name} {item.quantity} ))}
handleSearch(e.target.value)}
placeholder="Search..."
/>
{isPending &&
);
}
```
### useTransition with Server Actions
```typescript
'use client';
import { useTransition } from 'react';
import { deletePost } from '@/actions/posts';
export function DeleteButton({ postId }: { postId: string }) {
const [isPending, startTransition] = useTransition();
function handleDelete() {
startTransition(async () => {
await deletePost(postId);
// UI stays responsive during deletion
});
}
return (
);
}
```
## useDeferredValue - Deferred Rendering
Defer expensive re-renders while keeping UI responsive.
```typescript
'use client';
import { useDeferredValue, useState } from 'react';
export function ProductSearch() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
Searching...
}
-
{results.map((result) => (
- {result} ))}
setQuery(e.target.value)}
placeholder="Search products..."
/>
{/* Uses deferred value - won't block input */}
);
}
function ExpensiveResults({ query }: { query: string }) {
const results = useMemo(() => {
// Expensive filtering/sorting
return products.filter((p) => p.name.includes(query));
}, [query]);
return (
-
{results.map((result) => (
- {result.name} ))}
Source: claude-code-templates (MIT). See About Us for full credits.