Practical guidelines for implementing React 19 components with TypeScript
// components/UserProfile.tsx
interface UserProfileProps {
userId: string;
}
export async function UserProfile({ userId }: UserProfileProps) {
const user = await fetchUser(userId); // Direct database/API call
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Guidelines:
'use client';
import { useState } from 'react';
interface CounterProps {
initialCount?: number;
}
export function Counter({ initialCount = 0 }: CounterProps) {
const [count, setCount] = useState(initialCount);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
Guidelines:
Usage: Component-specific state
Tools:
useState for simple valuesuseReducer for complex state logicconst [isOpen, setIsOpen] = useState(false);
const [user, setUser] = useState<User | null>(null);
Usage: State shared across components
Approaches:
// Using Zustand store
import { useUserStore } from '@/stores/user';
export function UserMenu() {
const { user, logout } = useUserStore();
// ...
}
Usage: Data from API/database
Tool: TanStack React Query
'use client';
import { useQuery } from '@tanstack/react-query';
export function Posts() {
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
return <PostList posts={data} />;
}
Naming: Always prefix with 'use'
// hooks/useLocalStorage.ts
import { useState, useEffect } from 'react';
export function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
Best Practices:
| Hook | Purpose |
|---|---|
| useState | Local component state |
| useEffect | Side effects and subscriptions |
| useCallback | Memoize callback functions |
| useMemo | Memoize expensive calculations |
| useRef | Mutable refs and DOM access |
| useContext | Access context values |
'use client';
export function DataDisplay() {
const { data, error, isLoading } = useQuery(queryOptions);
if (error) {
return (
<ErrorAlert>
<p>Failed to load data</p>
<button onClick={() => refetch()}>Retry</button>
</ErrorAlert>
);
}
if (isLoading) return <LoadingSkeleton />;
return <DataView data={data} />;
}
<button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Click me
</button>
import styles from './Button.module.css';
<button className={styles.primary}>Click me</button>
import clsx from 'clsx';
<div className={clsx(
'base-class',
isActive && 'active',
isDisabled && 'disabled'
)}>
const [email, setEmail] = useState('');
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
Recommended: React Hook Form for complex forms
import { useForm } from 'react-hook-form';
const { register, handleSubmit, formState: { errors } } = useForm();
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email', { required: true })} />
{errors.email && <span>Email is required</span>}
</form>
Library: React Testing Library
Focus: Test user behavior, not implementation
import { render, screen, fireEvent } from '@testing-library/react';
import { Counter } from './Counter';
test('increments count on click', () => {
render(<Counter />);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(button).toHaveTextContent('Count: 1');
});