init
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
# Common Patterns
|
||||
|
||||
Frequently used patterns for forms, authentication, DataGrid, dialogs, and other common UI elements.
|
||||
|
||||
---
|
||||
|
||||
## Authentication with useAuth
|
||||
|
||||
### Getting Current User
|
||||
|
||||
```typescript
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
|
||||
export const MyComponent: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
|
||||
// Available properties:
|
||||
// - user.id: string
|
||||
// - user.email: string
|
||||
// - user.username: string
|
||||
// - user.roles: string[]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Logged in as: {user.email}</p>
|
||||
<p>Username: {user.username}</p>
|
||||
<p>Roles: {user.roles.join(', ')}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**NEVER make direct API calls for auth** - always use `useAuth` hook.
|
||||
|
||||
---
|
||||
|
||||
## Forms with React Hook Form
|
||||
|
||||
### Basic Form
|
||||
|
||||
```typescript
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { TextField, Button } from '@mui/material';
|
||||
import { useMuiSnackbar } from '@/hooks/useMuiSnackbar';
|
||||
|
||||
// Zod schema for validation
|
||||
const formSchema = z.object({
|
||||
username: z.string().min(3, 'Username must be at least 3 characters'),
|
||||
email: z.string().email('Invalid email address'),
|
||||
age: z.number().min(18, 'Must be 18 or older'),
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const MyForm: React.FC = () => {
|
||||
const { showSuccess, showError } = useMuiSnackbar();
|
||||
|
||||
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
username: '',
|
||||
email: '',
|
||||
age: 18,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: FormData) => {
|
||||
try {
|
||||
await api.submitForm(data);
|
||||
showSuccess('Form submitted successfully');
|
||||
} catch (error) {
|
||||
showError('Failed to submit form');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<TextField
|
||||
{...register('username')}
|
||||
label='Username'
|
||||
error={!!errors.username}
|
||||
helperText={errors.username?.message}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
{...register('email')}
|
||||
label='Email'
|
||||
error={!!errors.email}
|
||||
helperText={errors.email?.message}
|
||||
type='email'
|
||||
/>
|
||||
|
||||
<TextField
|
||||
{...register('age', { valueAsNumber: true })}
|
||||
label='Age'
|
||||
error={!!errors.age}
|
||||
helperText={errors.age?.message}
|
||||
type='number'
|
||||
/>
|
||||
|
||||
<Button type='submit' variant='contained'>
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dialog Component Pattern
|
||||
|
||||
### Standard Dialog Structure
|
||||
|
||||
From BEST_PRACTICES.md - All dialogs should have:
|
||||
- Icon in title
|
||||
- Close button (X)
|
||||
- Action buttons at bottom
|
||||
|
||||
```typescript
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, IconButton } from '@mui/material';
|
||||
import { Close, Info } from '@mui/icons-material';
|
||||
|
||||
interface MyDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export const MyDialog: React.FC<MyDialogProps> = ({ open, onClose, onConfirm }) => {
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth='sm' fullWidth>
|
||||
<DialogTitle>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Info color='primary' />
|
||||
Dialog Title
|
||||
</Box>
|
||||
<IconButton onClick={onClose} size='small'>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
{/* Content here */}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button onClick={onConfirm} variant='contained'>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DataGrid Wrapper Pattern
|
||||
|
||||
### Wrapper Component Contract
|
||||
|
||||
From BEST_PRACTICES.md - DataGrid wrappers should accept:
|
||||
|
||||
**Required Props:**
|
||||
- `rows`: Data array
|
||||
- `columns`: Column definitions
|
||||
- Loading/error states
|
||||
|
||||
**Optional Props:**
|
||||
- Toolbar components
|
||||
- Custom actions
|
||||
- Initial state
|
||||
|
||||
```typescript
|
||||
import { DataGridPro } from '@mui/x-data-grid-pro';
|
||||
import type { GridColDef } from '@mui/x-data-grid-pro';
|
||||
|
||||
interface DataGridWrapperProps {
|
||||
rows: any[];
|
||||
columns: GridColDef[];
|
||||
loading?: boolean;
|
||||
toolbar?: React.ReactNode;
|
||||
onRowClick?: (row: any) => void;
|
||||
}
|
||||
|
||||
export const DataGridWrapper: React.FC<DataGridWrapperProps> = ({
|
||||
rows,
|
||||
columns,
|
||||
loading = false,
|
||||
toolbar,
|
||||
onRowClick,
|
||||
}) => {
|
||||
return (
|
||||
<DataGridPro
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
slots={{ toolbar: toolbar ? () => toolbar : undefined }}
|
||||
onRowClick={(params) => onRowClick?.(params.row)}
|
||||
// Standard configuration
|
||||
pagination
|
||||
pageSizeOptions={[25, 50, 100]}
|
||||
initialState={{
|
||||
pagination: { paginationModel: { pageSize: 25 } },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mutation Patterns
|
||||
|
||||
### Update with Cache Invalidation
|
||||
|
||||
```typescript
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMuiSnackbar } from '@/hooks/useMuiSnackbar';
|
||||
|
||||
export const useUpdateEntity = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { showSuccess, showError } = useMuiSnackbar();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: any }) =>
|
||||
api.updateEntity(id, data),
|
||||
|
||||
onSuccess: (result, variables) => {
|
||||
// Invalidate affected queries
|
||||
queryClient.invalidateQueries({ queryKey: ['entity', variables.id] });
|
||||
queryClient.invalidateQueries({ queryKey: ['entities'] });
|
||||
|
||||
showSuccess('Entity updated');
|
||||
},
|
||||
|
||||
onError: () => {
|
||||
showError('Failed to update entity');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Usage
|
||||
const updateEntity = useUpdateEntity();
|
||||
|
||||
const handleSave = () => {
|
||||
updateEntity.mutate({ id: 123, data: { name: 'New Name' } });
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State Management Patterns
|
||||
|
||||
### TanStack Query for Server State (PRIMARY)
|
||||
|
||||
Use TanStack Query for **all server data**:
|
||||
- Fetching: useSuspenseQuery
|
||||
- Mutations: useMutation
|
||||
- Caching: Automatic
|
||||
- Synchronization: Built-in
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT - TanStack Query for server data
|
||||
const { data: users } = useSuspenseQuery({
|
||||
queryKey: ['users'],
|
||||
queryFn: () => userApi.getUsers(),
|
||||
});
|
||||
```
|
||||
|
||||
### useState for UI State
|
||||
|
||||
Use `useState` for **local UI state only**:
|
||||
- Form inputs (uncontrolled)
|
||||
- Modal open/closed
|
||||
- Selected tab
|
||||
- Temporary UI flags
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT - useState for UI state
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [selectedTab, setSelectedTab] = useState(0);
|
||||
```
|
||||
|
||||
### Zustand for Global Client State (Minimal)
|
||||
|
||||
Use Zustand only for **global client state**:
|
||||
- Theme preference
|
||||
- Sidebar collapsed state
|
||||
- User preferences (not from server)
|
||||
|
||||
```typescript
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface AppState {
|
||||
sidebarOpen: boolean;
|
||||
toggleSidebar: () => void;
|
||||
}
|
||||
|
||||
export const useAppState = create<AppState>((set) => ({
|
||||
sidebarOpen: true,
|
||||
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
||||
}));
|
||||
```
|
||||
|
||||
**Avoid prop drilling** - use context or Zustand instead.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Common Patterns:**
|
||||
- ✅ useAuth hook for current user (id, email, roles, username)
|
||||
- ✅ React Hook Form + Zod for forms
|
||||
- ✅ Dialog with icon + close button
|
||||
- ✅ DataGrid wrapper contracts
|
||||
- ✅ Mutations with cache invalidation
|
||||
- ✅ TanStack Query for server state
|
||||
- ✅ useState for UI state
|
||||
- ✅ Zustand for global client state (minimal)
|
||||
|
||||
**See Also:**
|
||||
- [data-fetching.md](data-fetching.md) - TanStack Query patterns
|
||||
- [component-patterns.md](component-patterns.md) - Component structure
|
||||
- [loading-and-error-states.md](loading-and-error-states.md) - Error handling
|
||||
Reference in New Issue
Block a user