Feature CRUD Complète

Zustand, React Query, React Hook Form + Zod

Architecture feature-based

Utilisez les flèches, cliquez ou glissez pour naviguer

Objectifs de la leçon

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

Plan du cours

1

Recap semaine 5

Zustand, API client, React Query, React Hook Form + Zod

2

Architecture feature-based

Un dossier par feature avec tout dedans

3

Le flow CRUD complet

List → Create → Update → Delete

4

Zustand + React Query

Comment les connecter correctement

5

Mini-projet et démo

Objectif final

Recap Semaine 5

Les outils qu'on a appris

Zustand

Client state

React Query

Server state

React Hook Form

Formulaires

Zod

Validation

Zustand : Client State

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

API Client : Axios

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);

React Query : Server State

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']

})

});

React Hook Form + Zod

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)

});

Architecture Feature-Based

Un dossier par feature

Organiser le code par domaine métier

Pas par type de fichier!

❌ Le problème de la structure classique

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!

✅ Solution : Feature-Based

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

Avantages de l'architecture feature-based

✅ 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

Structure détaillée d'une 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

Le Flow CRUD Complet

Create, Read, Update, Delete

List

Afficher

Create

Créer

Update

Modifier

Delete

Supprimer

1️⃣ List : Afficher les données

// 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 />;

2️⃣ Create : Créer une entité

// 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

3️⃣ Update : Modifier une entité

// 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'] });

}

});

}

4️⃣ Delete : Supprimer une entité

// hooks/useDeleteUser.ts

export const useDeleteUser = () => {

const queryClient = useQueryClient();

return useMutation({

mutationFn: deleteUser,

onSuccess: () => {

queryClient.invalidateQueries({ queryKey: ['users'] });

}

});

}

💡 Pattern : onSuccess → invalider le cache

Zustand + React Query

Comment les connecter correctement

Chacun a sa responsabilité

Client State vs Server State

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

❌ Erreur courante : Mettre les données dans Zustand

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

✅ Bonne pratique : Séparer les responsabilités

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

});

Exemple : Connecter les deux

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

Le Flow Complet

Service → Hook → Component

Chaque couche a une responsabilité claire

1️⃣ Couche Service : Appels API

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}`),

}

2️⃣ Couche Hook : React Query

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']

})

});

}

3️⃣ Couche Component : UI

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>

);

}

Schéma du Flow

Service

Appels API bruts

Hook

React Query + Zustand

Component

UI + Events

💡 Unidirectionnel : les données descendent, les events remontent

Formulaires CRUD

React Hook Form + Zod + useMutation

Le formulaire crée via useMutation

Pas en poussant dans Zustand!

Schema Zod pour User

// 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>;

Formulaire Create

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>

);

};

Formulaire Update avec valeurs par défaut

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

⚠️ Pièges Courants

Éviter ces erreurs

❌ Piège 1 : Données serveur dans Zustand

❌ Mauvais

users: [] // dans Zustand

Pas de cache, pas de refetch

✅ Bon

useQuery({ queryKey: ['users'] })

Cache, refetch, sync automatiques

❌ Piège 2 : API dans les composants

❌ 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 });

❌ Piège 3 : Oublier d'invalider le cache

❌ 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!

❌ Piège 4 : Ne pas gérer loading/error

❌ 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!

🎯 Mini-Projet

Gestionnaire d'utilisateurs CRUD

Mettre en pratique tout ce qu'on a appris

Objectif Final

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

Structure du projet

📁 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

Points clés à retenir

📁 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

À retenir!

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!

Exercices pratiques

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

Questions?

Architecture feature-based = code maintenable

🎯 Maintenant, place à la pratique!