React Frontend with TypeScript
Build a type-safe React frontend — typed components, hooks, API client, and state management using shared types from the backend.
50 min•By Priygop Team•Last updated: Feb 2026
Typed React Components
- Props Interface: interface UserCardProps { user: User; onEdit: (id: number) => void; onDelete: (id: number) => void; isLoading?: boolean; } const UserCard: React.FC<UserCardProps> = ({ user, onEdit, onDelete, isLoading = false }) => { ... } — all props type-checked
- Generic Components: interface DataTableProps<T> { data: T[]; columns: Column<T>[]; onRowClick?: (item: T) => void; } function DataTable<T extends { id: number }>({ data, columns, onRowClick }: DataTableProps<T>) { ... } — reusable table component for any entity
- Event Handlers: const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); const formData = new FormData(e.currentTarget); }. onChange uses React.ChangeEvent<HTMLInputElement>. onClick uses React.MouseEvent<HTMLButtonElement>
- Children Pattern: interface LayoutProps { children: React.ReactNode; title: string; sidebar?: React.ReactNode; } — children is ReactNode (most permissive), sidebar is optional. Use React.ReactElement for stricter typing
- Discriminated Union Props: type ButtonProps = { variant: 'link'; href: string; } | { variant: 'button'; onClick: () => void; } — if variant is 'link', href is required. If 'button', onClick is required. TypeScript enforces correct props per variant
- Ref Forwarding: const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => <input ref={ref} {...props} />) — properly typed ref forwards for form libraries like React Hook Form
API Client & Data Fetching
- Typed Fetch Wrapper: async function api<T>(url: string, options?: RequestInit): Promise<ApiResponse<T>> { const res = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', ...options?.headers } }); return res.json(); } — every API call returns typed ApiResponse<T>
- API Functions: const userApi = { getAll: () => api<User[]>(API_ROUTES.users.list), getById: (id: number) => api<User>(API_ROUTES.users.getById(id)), create: (data: CreateUserDTO) => api<User>(API_ROUTES.users.create, { method: 'POST', body: JSON.stringify(data) }) } — fully typed API layer using shared route definitions
- Custom Hook: function useUsers() { const [users, setUsers] = useState<User[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { userApi.getAll().then(res => { if (res.success && res.data) setUsers(res.data); else setError(res.error ?? 'Unknown error'); }).finally(() => setLoading(false)); }, []); return { users, loading, error }; }
- Generic Data Hook: function useQuery<T>(fetcher: () => Promise<ApiResponse<T>>) { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); ... return { data, loading, error, refetch }; } — reusable for any API endpoint
- Form State: interface FormState<T> { values: T; errors: Partial<Record<keyof T, string>>; isSubmitting: boolean; } function useForm<T>(initial: T) { const [state, setState] = useState<FormState<T>>({ values: initial, errors: {}, isSubmitting: false }); ... } — type-safe form management
- Type Guards: function isApiError(response: ApiResponse<unknown>): response is ApiResponse<never> & { error: string } { return !response.success && !!response.error; } — narrow response type after checking success flag