phase-6-ui-integration
Installation
SKILL.md
Phase 6: UI Integration
Connect frontend components with backend APIs and implement state management.
Purpose
Phase 6 bridges the gap between Phase 5 (design system) and Phase 4 (API). This phase wires up data fetching, state management, error handling, and user interactions to create a fully functional application. Every page becomes alive with real data.
Actions
| Action | Description | Example |
|---|---|---|
start |
Begin Phase 6 | $phase-6-ui-integration start |
api-client |
Set up API client | $phase-6-ui-integration api-client |
state |
Implement state management | $phase-6-ui-integration state |
forms |
Set up form handling | $phase-6-ui-integration forms |
review |
Review integration quality | $phase-6-ui-integration review |
Deliverables
- API Client - Configured HTTP client with interceptors and error handling
- Data Fetching Hooks - TanStack Query hooks for each API resource
- State Management - Global state stores (Zustand) for cross-cutting concerns
- Page Implementations - All pages connected with real data
- Form Handling - Validated forms with react-hook-form + Zod
- Error Boundaries - Graceful error handling at every level
- Loading States - Skeleton loaders and progress indicators
src/
├── app/ # Pages (Next.js App Router)
│ ├── layout.tsx # Root layout with providers
│ ├── page.tsx # Home page
│ ├── providers.tsx # QueryClient, theme, etc.
│ ├── (auth)/ # Auth route group
│ │ ├── login/page.tsx
│ │ └── register/page.tsx
│ └── (main)/ # Main route group
│ ├── layout.tsx # Authenticated layout
│ ├── dashboard/page.tsx
│ └── settings/page.tsx
├── features/ # Feature modules
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ └── services/
│ └── {feature}/
│ ├── components/
│ ├── hooks/
│ └── services/
├── hooks/ # Shared hooks
│ ├── useAuth.ts
│ ├── useDebounce.ts
│ └── useLocalStorage.ts
├── stores/ # Global state (Zustand)
│ ├── auth-store.ts
│ └── ui-store.ts
└── lib/
├── api-client.ts # Axios/fetch wrapper
└── query-keys.ts # TanStack Query key factory
Process
Step 1: API Client Setup
Create a centralized API client with request/response interceptors:
// lib/api-client.ts
import axios from 'axios';
import { useAuthStore } from '@/stores/auth-store';
const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
timeout: 10000,
headers: { 'Content-Type': 'application/json' },
});
apiClient.interceptors.request.use((config) => {
const token = useAuthStore.getState().accessToken;
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
useAuthStore.getState().logout();
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default apiClient;
Step 2: Data Fetching with TanStack Query
Query Key Factory
// lib/query-keys.ts
export const queryKeys = {
users: {
all: ['users'] as const,
list: (filters?: UserFilters) => ['users', 'list', filters] as const,
detail: (id: string) => ['users', 'detail', id] as const,
},
posts: {
all: ['posts'] as const,
list: (filters?: PostFilters) => ['posts', 'list', filters] as const,
detail: (id: string) => ['posts', 'detail', id] as const,
},
};
Query and Mutation Hooks
// features/users/hooks/useUsers.ts
export function useUsers(filters?: UserFilters) {
return useQuery({
queryKey: queryKeys.users.list(filters),
queryFn: () => userService.list(filters),
staleTime: 5 * 60 * 1000,
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: userService.create,
onSuccess: () => queryClient.invalidateQueries({ queryKey: queryKeys.users.all }),
});
}
Provider Setup
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: { staleTime: 60 * 1000, retry: 1, refetchOnWindowFocus: false },
},
}));
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}
Step 3: State Management with Zustand
// stores/auth-store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface AuthState {
user: User | null;
accessToken: string | null;
isAuthenticated: boolean;
setAuth: (user: User, token: string) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: null, accessToken: null, isAuthenticated: false,
setAuth: (user, accessToken) => set({ user, accessToken, isAuthenticated: true }),
logout: () => set({ user: null, accessToken: null, isAuthenticated: false }),
}),
{ name: 'auth-storage' }
)
);
Step 4: Optimistic Updates
export function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: userService.delete,
onMutate: async (deletedId) => {
await queryClient.cancelQueries({ queryKey: queryKeys.users.all });
const previous = queryClient.getQueryData(queryKeys.users.list());
queryClient.setQueryData(queryKeys.users.list(), (old: User[] | undefined) =>
old?.filter((user) => user.id !== deletedId)
);
return { previous };
},
onError: (_err, _id, context) => {
queryClient.setQueryData(queryKeys.users.list(), context?.previous);
},
onSettled: () => queryClient.invalidateQueries({ queryKey: queryKeys.users.all }),
});
}
Step 5: Form Handling with react-hook-form + Zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const createUserSchema = z.object({
email: z.string().email('Invalid email address'),
name: z.string().min(2, 'Name must be at least 2 characters').max(100),
role: z.enum(['user', 'admin']).default('user'),
});
type CreateUserInput = z.infer<typeof createUserSchema>;
export function CreateUserForm({ onSuccess }: { onSuccess?: () => void }) {
const { register, handleSubmit, formState: { errors }, reset } = useForm<CreateUserInput>({
resolver: zodResolver(createUserSchema),
});
const createUser = useCreateUser();
return (
<form onSubmit={handleSubmit((data) => createUser.mutate(data, {
onSuccess: () => { reset(); onSuccess?.(); },
}))} className="space-y-4">
<FormField label="Email" error={errors.email?.message} required>
<Input type="email" {...register('email')} />
</FormField>
<FormField label="Name" error={errors.name?.message} required>
<Input {...register('name')} />
</FormField>
<Button type="submit" isLoading={createUser.isPending}>Create User</Button>
</form>
);
}
Step 6: Error and Loading States
// components/patterns/AsyncState.tsx
interface AsyncStateProps<T> {
data: T | undefined;
isLoading: boolean;
error: Error | null;
children: (data: T) => React.ReactNode;
}
export function AsyncState<T>({ data, isLoading, error, children }: AsyncStateProps<T>) {
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
if (!data || (Array.isArray(data) && data.length === 0)) return <EmptyState />;
return <>{children(data)}</>;
}
Level-wise Application
| Level | Integration Scope |
|---|---|
| Starter | Static pages with no API; content hardcoded or from CMS |
| Dynamic | Full API integration with TanStack Query, Zustand, form validation |
| Enterprise | Multi-service integration, real-time (WebSocket/SSE), optimistic updates, complex caching |
Integration Patterns
See references/integration-patterns.md for detailed patterns:
- Data fetching strategy matrix (server-side, client-side, optimistic, real-time)
- Loading state pattern with skeletons
- Form handling with react-hook-form + Zod
- Route protection middleware
- Toast notification store
PDCA Application
- Plan: Map every page to its API endpoints and state requirements
- Design: Define data flow diagrams and state structure
- Do: Implement pages with API integration, one feature at a time
- Check: Test all user flows end-to-end; verify error states
- Act: Fix issues, optimize performance, proceed to Phase 7
Common Mistakes
| Mistake | Solution |
|---|---|
| Fetching in useEffect | Use TanStack Query for all server state |
| Global state for server data | Server state belongs in TanStack Query cache |
| No loading states | Always handle loading, error, and empty states |
| Missing error boundaries | Add error.tsx in every route segment |
| No optimistic updates | Use for delete/toggle operations for instant feedback |
| Prop drilling | Use Zustand stores or React Context for shared state |
Output Location
docs/02-design/
└── integration-spec.md # Integration specifications
src/features/ # Feature modules with hooks and services
src/stores/ # Zustand stores
src/lib/api-client.ts # API client
Next Phase
When integration is complete, proceed to $phase-7-seo-security to optimize for search engines and harden security.
Related skills