Zustand, React Query, React Hook Form + Zod
Architecture feature-based
Utilisez les flèches, cliquez ou glissez pour naviguer
1. Architecture feature-based
Organiser le code par feature
2. Connecter Zustand + React Query
Client state vs Server state
3. Implémenter le flow CRUD
List, create, update, delete
4. Structurer une feature
Fichiers maintenables
Recap semaine 5
Zustand, API client, React Query, React Hook Form + Zod
Architecture feature-based
Un dossier par feature avec tout dedans
Le flow CRUD complet
List → Create → Update → Delete
Zustand + React Query
Comment les connecter correctement
Mini-projet et démo
Objectif final
Les outils qu'on a appris
Zustand
Client state
React Query
Server state
React Hook Form
Formulaires
Zod
Validation
State management simple et léger
// Créer un store
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({
count: state.count + 1
})),
}))
💡 Usage : UI state, filtres, préférences utilisateur
Centraliser les appels HTTP
// api/client.ts
const api = axios.create({
baseURL: '/api',
headers: { 'Content-Type': 'application/json' }
});
// Usage
const { data } = await api.get('/users');
const { data } = await api.post('/users', newUser);
Cache, refetch, mutations automatiques
// Fetch data
const { data, isLoading, error } =
useQuery({
queryKey: ['users'],
queryFn: () => api.get('/users'),
});
// Mutation
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => queryClient.invalidateQueries({
queryKey: ['users']
})
});
Formulaires performants avec validation
// Schema Zod
const schema = z.object({
name: z.string().min(2, "Min 2 chars"),
email: z.string().email(),
});
// Hook Form avec Zod
const { register, handleSubmit, formState } =
useForm({
resolver: zodResolver(schema)
});
Un dossier par feature
Organiser le code par domaine métier
Pas par type de fichier!
Organiser par type de fichier
// ❌ Structure par type
📁 components/
📄 UserCard.tsx
📄 UserForm.tsx
📄 UserList.tsx
📁 hooks/
📄 useUsers.ts
📁 services/
📄 userService.ts
📁 types/
📄 user.ts
⚠️ Pour ajouter une feature, on touche 4-5 dossiers différents!
Tout ce qui concerne une feature dans un seul dossier
// ✅ Structure feature-based
📁 features/
📁 users/
📁 components/
📄 UserCard.tsx
📄 UserForm.tsx
📄 UserList.tsx
📁 hooks/
📄 useUsers.ts
📁 services/
📄 userService.ts
📄 types.ts
📄 schemas.ts
📄 index.ts
✅ Cohésion maximale
Tout ce qui concerne "users" est au même endroit
✅ Facile à naviguer
Un seul dossier à ouvrir pour une feature
✅ Facile à supprimer
Supprimer un dossier = supprimer une feature
✅ Réutilisable
Copier le dossier = copier la feature
📁 components/
UI components de la feature
📁 hooks/
Custom hooks (React Query, Zustand)
📁 services/
Appels API
📄 types.ts
Types TypeScript
📄 schemas.ts
Schemas Zod
📄 store.ts
Store Zustand (si besoin)
📄 index.ts = Export public de la feature
Create, Read, Update, Delete
List
Afficher
Create
Créer
Update
Modifier
Delete
Supprimer
// hooks/useUsers.ts
export const useUsers = () => {
return useQuery({
queryKey: ['users'],
queryFn: getUsers,
});
}
// components/UserList.tsx
const { data: users, isLoading, error } = useUsers();
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage />;
// hooks/useCreateUser.ts
export const useCreateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
}
});
}
✅ Invalider le cache = Rafraîchir automatiquement la liste
// hooks/useUpdateUser.ts
export const useUpdateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateUser,
onSuccess: (data) => {
queryClient.setQueryData(['users', data.id], data);
queryClient.invalidateQueries({ queryKey: ['users'] });
}
});
}
// hooks/useDeleteUser.ts
export const useDeleteUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
}
});
}
💡 Pattern : onSuccess → invalider le cache
Comment les connecter correctement
Chacun a sa responsabilité
Zustand
Client State
✅ État de l'UI (modals ouverts)
✅ Filtres actifs
✅ Préférences utilisateur
✅ Thème (dark/light)
✅ Formulaire temporaire
React Query
Server State
✅ Données du serveur
✅ Liste des utilisateurs
✅ Détails d'un user
✅ Cache automatique
✅ Synchronisation
Stocker les données du serveur dans Zustand
// ❌ MAUVAIS
const useStore = create((set) => ({
users: [], // ❌ Données serveur!
setUsers: (users) => set({ users }),
}))
⚠️ Problèmes : pas de cache, pas de refetch, pas de synchronisation
Zustand pour l'UI, React Query pour les données
// ✅ BON - Zustand pour UI state
const useUIStore = create((set) => ({
selectedUserId: null, // ✅ UI state
isModalOpen: false, // ✅ UI state
filter: 'all', // ✅ UI state
}))
// ✅ BON - React Query pour données
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: getUsers
});
const UserList = () => {
// Server state via React Query
const { data: users } = useUsers();
// Client state via Zustand
const { filter, setFilter } = useUIStore();
// Filtrer côté client
const filteredUsers = users?.filter(u =>
filter === 'all' || u.role === filter
);
};
💡 React Query gère les données, Zustand gère le filtre
Service → Hook → Component
Chaque couche a une responsabilité claire
Isoler la logique API
// services/userService.ts
export const userService = {
getAll: () => api.get('/users'),
getById: (id) => api.get(`/users/${id}`),
create: (data) => api.post('/users', data),
update: (id, data) => api.put(`/users/${id}`, data),
delete: (id) => api.delete(`/users/${id}`),
}
Wrapper les services avec React Query
// hooks/useUsers.ts
export const useUsers = () => useQuery({
queryKey: ['users'],
queryFn: userService.getAll,
});
export const useCreateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: userService.create,
onSuccess: () => queryClient.invalidateQueries({
queryKey: ['users']
})
});
}
Utiliser les hooks, gérer l'affichage
// components/UserList.tsx
export const UserList = () => {
const { data: users, isLoading } = useUsers();
const { mutate: createUser } = useCreateUser();
if (isLoading) return <Spinner />;
return (
<div>
{users?.map(user => <UserCard key={user.id} user={user} />)}
</div>
);
}
Service
Appels API bruts
↓
Hook
React Query + Zustand
↓
Component
UI + Events
💡 Unidirectionnel : les données descendent, les events remontent
React Hook Form + Zod + useMutation
Le formulaire crée via useMutation
Pas en poussant dans Zustand!
// schemas.ts
import { z } from 'zod';
export const userSchema = z.object({
name: z.string()
.min(2, "Le nom doit avoir au moins 2 caractères")
.max(50),
email: z.string()
.email("Email invalide"),
role: z.enum(['admin', 'user']),
});
export type UserInput = z.infer<typeof userSchema>;
const UserForm = () => {
const { register, handleSubmit, formState: { errors } } =
useForm({ resolver: zodResolver(userSchema) });
const { mutate, isPending } = useCreateUser();
const onSubmit = (data) => mutate(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
<button disabled={isPending}>
{isPending ? 'Création...' : 'Créer'}
</button>
</form>
);
};
const EditUserForm = ({ user }) => {
const { register, handleSubmit } = useForm({
resolver: zodResolver(userSchema),
defaultValues: user // Pré-remplir!
});
const { mutate } = useUpdateUser();
const onSubmit = (data) => mutate({ id: user.id, ...data });
// ... même JSX
};
💡 defaultValues pré-remplit le formulaire avec les données existantes
Éviter ces erreurs
❌ Mauvais
users: [] // dans Zustand
Pas de cache, pas de refetch
✅ Bon
useQuery({ queryKey: ['users'] })
Cache, refetch, sync automatiques
❌ Mauvais
const UserList = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users')
.then(r => r.json())
.then(setUsers);
}, []);
};
✅ Bon
// Service isolé
export const getUsers = () =>
api.get('/users');
// Hook dédié
export const useUsers = () =>
useQuery({ queryFn: getUsers });
❌ Mauvais
useMutation({
mutationFn: createUser
});
La liste ne se rafraîchit pas!
✅ Bon
useMutation({
mutationFn: createUser,
onSuccess: () =>
queryClient.invalidateQueries({
queryKey: ['users']
})
});
La liste se rafraîchit automatiquement!
❌ Mauvais
const { data } = useUsers();
return <div>{data}.map(...)</div>;
Crash si data est undefined!
✅ Bon
const { data, isLoading, error } = useUsers();
if (isLoading) return <Spinner />;
if (error) return <Error />;
return <div>{data}.map(...)</div>;
UX propre et sans crash!
Gestionnaire d'utilisateurs CRUD
Mettre en pratique tout ce qu'on a appris
Feature complète avec :
📋 Liste des utilisateurs
avec filtres (Zustand)
➕ Créer un utilisateur
Formulaire validé (Zod)
✏️ Modifier un utilisateur
Pré-remplissage
🗑️ Supprimer un utilisateur
Confirmation
🏗️ Architecture feature-based complète
📁 features/
📁 users/
📁 components/
📄 UserList.tsx
📄 UserCard.tsx
📄 UserForm.tsx
📄 UserFilters.tsx
📁 hooks/
📄 useUsers.ts
📄 useCreateUser.ts
📄 useUpdateUser.ts
📄 useDeleteUser.ts
📁 services/
📄 userService.ts
📄 types.ts
📄 schemas.ts
📄 store.ts
📄 index.ts
📁 Une feature = un dossier
hooks, components, services, schemas, types
🔄 Zustand = client state, React Query = server state
Ne pas mélanger!
📝 Formulaire = useMutation + invalidation
Pas de push dans Zustand
🧱 Couches : service → hook → component
Responsabilité claire pour chaque couche
Architecture
Feature-based = tout au même endroit
State
Zustand (UI) + React Query (data)
CRUD
useMutation + invalidateQueries
Flow
Service → Hook → Component
⚠️ Ne pas oublier d'invalider le cache après une mutation!
1. Créer la structure feature-based
Dossiers et fichiers pour la feature "users"
2. Implémenter le service API
CRUD complet avec axios
3. Créer les hooks React Query
useUsers, useCreateUser, useUpdateUser, useDeleteUser
4. Construire les composants UI
Liste, formulaire, filtres avec Zustand
Architecture feature-based = code maintenable
🎯 Maintenant, place à la pratique!