Awesome-omni-skill frontend-patterns
Frontend development and API integration patterns for React, TypeScript, and state management
install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/frontend-patterns" ~/.claude/skills/diegosouzapw-awesome-omni-skill-frontend-patterns-7c305c && rm -rf "$T"
manifest:
skills/development/frontend-patterns/SKILL.mdsource content
Frontend Patterns Skill
Purpose
Build robust frontend applications with proper API integration and state management.
Data Fetching Patterns
TanStack Query (React Query)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; // Query configuration const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, // 5 minutes gcTime: 30 * 60 * 1000, // 30 minutes (formerly cacheTime) retry: 3, retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000), refetchOnWindowFocus: false, }, }, }); // Type-safe API client const api = { users: { list: async (params: { page: number; limit: number }) => { const res = await fetch(`/api/users?${new URLSearchParams(params as any)}`); if (!res.ok) throw new ApiError(res); return res.json() as Promise<PaginatedResponse<User>>; }, get: async (id: string) => { const res = await fetch(`/api/users/${id}`); if (!res.ok) throw new ApiError(res); return res.json() as Promise<User>; }, create: async (data: CreateUserInput) => { const res = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!res.ok) throw new ApiError(res); return res.json() as Promise<User>; }, }, }; // Query hook with pagination function useUsers(page: number) { return useQuery({ queryKey: ['users', 'list', { page }], queryFn: () => api.users.list({ page, limit: 20 }), placeholderData: (prev) => prev, // Keep previous data while loading }); } // Single user query function useUser(id: string) { return useQuery({ queryKey: ['users', 'detail', id], queryFn: () => api.users.get(id), enabled: !!id, }); } // Mutation with optimistic update function useCreateUser() { const queryClient = useQueryClient(); return useMutation({ mutationFn: api.users.create, onMutate: async (newUser) => { // Cancel outgoing refetches await queryClient.cancelQueries({ queryKey: ['users', 'list'] }); // Snapshot previous value const previous = queryClient.getQueryData(['users', 'list']); // Optimistically update queryClient.setQueryData(['users', 'list'], (old: any) => ({ ...old, data: [...(old?.data || []), { ...newUser, id: 'temp-id' }], })); return { previous }; }, onError: (err, newUser, context) => { // Rollback on error queryClient.setQueryData(['users', 'list'], context?.previous); }, onSettled: () => { // Refetch after mutation queryClient.invalidateQueries({ queryKey: ['users', 'list'] }); }, }); }
SWR Pattern
import useSWR, { mutate } from 'swr'; import useSWRMutation from 'swr/mutation'; const fetcher = async (url: string) => { const res = await fetch(url); if (!res.ok) throw new Error('Failed to fetch'); return res.json(); }; function useUsers() { const { data, error, isLoading, isValidating } = useSWR<User[]>( '/api/users', fetcher, { revalidateOnFocus: false, dedupingInterval: 5000, } ); return { users: data, isLoading, isRefreshing: isValidating && data, error, }; } // SWR Mutation function useCreateUser() { return useSWRMutation( '/api/users', async (url: string, { arg }: { arg: CreateUserInput }) => { const res = await fetch(url, { method: 'POST', body: JSON.stringify(arg), }); return res.json(); }, { onSuccess: () => mutate('/api/users'), } ); }
State Management
Zustand (Recommended for most cases)
import { create } from 'zustand'; import { persist, devtools } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; interface AuthState { user: User | null; token: string | null; isAuthenticated: boolean; login: (credentials: Credentials) => Promise<void>; logout: () => void; updateUser: (updates: Partial<User>) => void; } const useAuthStore = create<AuthState>()( devtools( persist( immer((set, get) => ({ user: null, token: null, isAuthenticated: false, login: async (credentials) => { const response = await api.auth.login(credentials); set((state) => { state.user = response.user; state.token = response.token; state.isAuthenticated = true; }); }, logout: () => { set((state) => { state.user = null; state.token = null; state.isAuthenticated = false; }); }, updateUser: (updates) => { set((state) => { if (state.user) { Object.assign(state.user, updates); } }); }, })), { name: 'auth-store' } ), { name: 'Auth' } ) ); // Selectors (prevent unnecessary re-renders) const useUser = () => useAuthStore((state) => state.user); const useIsAuthenticated = () => useAuthStore((state) => state.isAuthenticated);
Redux Toolkit (Enterprise)
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; // Async thunk export const fetchUsers = createAsyncThunk( 'users/fetchAll', async (params: { page: number }, { rejectWithValue }) => { try { return await api.users.list(params); } catch (error) { return rejectWithValue(error.message); } } ); // Slice const usersSlice = createSlice({ name: 'users', initialState: { items: [] as User[], status: 'idle' as 'idle' | 'loading' | 'succeeded' | 'failed', error: null as string | null, pagination: { page: 1, total: 0 }, }, reducers: { userAdded: (state, action: PayloadAction<User>) => { state.items.push(action.payload); }, userUpdated: (state, action: PayloadAction<User>) => { const index = state.items.findIndex((u) => u.id === action.payload.id); if (index !== -1) { state.items[index] = action.payload; } }, }, extraReducers: (builder) => { builder .addCase(fetchUsers.pending, (state) => { state.status = 'loading'; }) .addCase(fetchUsers.fulfilled, (state, action) => { state.status = 'succeeded'; state.items = action.payload.data; state.pagination = action.payload.pagination; }) .addCase(fetchUsers.rejected, (state, action) => { state.status = 'failed'; state.error = action.payload as string; }); }, }); export const { userAdded, userUpdated } = usersSlice.actions; export default usersSlice.reducer;
Error Handling
// Custom error class class ApiError extends Error { constructor( public response: Response, public data?: { type: string; title: string; detail?: string } ) { super(data?.title || 'API Error'); this.name = 'ApiError'; } static async fromResponse(response: Response): Promise<ApiError> { const data = await response.json().catch(() => null); return new ApiError(response, data); } } // Error boundary component function QueryErrorBoundary({ children }: { children: React.ReactNode }) { const queryClient = useQueryClient(); return ( <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={({ error, resetErrorBoundary }) => ( <div className="error-container"> <h2>Something went wrong</h2> <p>{error.message}</p> <button onClick={resetErrorBoundary}>Try again</button> </div> )} > {children} </ErrorBoundary> )} </QueryErrorResetBoundary> ); } // Hook with error handling function useUsersSafe(page: number) { const query = useUsers(page); useEffect(() => { if (query.error instanceof ApiError) { if (query.error.response.status === 401) { // Redirect to login router.push('/login'); } else if (query.error.response.status >= 500) { // Show toast toast.error('Server error. Please try again later.'); } } }, [query.error]); return query; }
Optimistic Updates Pattern
function useTodoToggle() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (todo: Todo) => api.todos.update(todo.id, { completed: !todo.completed }), onMutate: async (todo) => { await queryClient.cancelQueries({ queryKey: ['todos'] }); const previous = queryClient.getQueryData<Todo[]>(['todos']); queryClient.setQueryData<Todo[]>(['todos'], (old) => old?.map((t) => t.id === todo.id ? { ...t, completed: !t.completed } : t ) ); return { previous }; }, onError: (err, todo, context) => { queryClient.setQueryData(['todos'], context?.previous); toast.error('Failed to update todo'); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }); }, }); }
Unit Test Template
import { describe, it, expect, vi } from 'vitest'; import { renderHook, waitFor } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; const createWrapper = () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); return ({ children }: { children: React.ReactNode }) => ( <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> ); }; describe('Frontend Patterns', () => { describe('useUsers hook', () => { it('should fetch and return users', async () => { vi.spyOn(global, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: [{ id: '1', name: 'Test' }] }), } as Response); const { result } = renderHook(() => useUsers(1), { wrapper: createWrapper(), }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data?.data).toHaveLength(1); }); it('should handle errors', async () => { vi.spyOn(global, 'fetch').mockResolvedValue({ ok: false, status: 500, } as Response); const { result } = renderHook(() => useUsers(1), { wrapper: createWrapper(), }); await waitFor(() => expect(result.current.isError).toBe(true)); }); }); describe('Zustand store', () => { it('should update auth state on login', async () => { const { result } = renderHook(() => useAuthStore()); await result.current.login({ email: 'test@test.com', password: 'pass' }); expect(result.current.isAuthenticated).toBe(true); expect(result.current.user).toBeDefined(); }); it('should clear state on logout', () => { const { result } = renderHook(() => useAuthStore()); result.current.logout(); expect(result.current.isAuthenticated).toBe(false); expect(result.current.user).toBeNull(); }); }); });
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| Infinite refetching | Missing dependency array | Use stable queryKey |
| Stale data shown | staleTime too high | Reduce staleTime or invalidate |
| Memory leak | Unmounted component | Use cleanup in useEffect |
| Too many re-renders | Non-memoized selectors | Use shallow comparison |
| Optimistic rollback fails | Missing previous snapshot | Always capture previous state |
Quality Checklist
- Data fetching with TanStack Query or SWR
- Type-safe API client
- Error boundaries configured
- Loading states handled
- Optimistic updates for mutations
- Cache invalidation strategy
- State persistence (where needed)
- Memoization applied
- Tests for hooks and stores