React - Gestion des APIs et des Données
Salut ! Maintenant qu'on maîtrise les bases de React, on va voir comment récupérer, gérer et synchroniser les données avec des APIs externes !
Le problème avec les APIs
Avant, on faisait n'importe quoi
// ❌ Le code classique qui pue (mais qu'on voit partout)
function ProduitsList() {
const [produits, setProduits] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch('/api/produits')
.then(res => res.json())
.then(data => {
setProduits(data)
setLoading(false)
})
.catch(err => {
setError(err)
setLoading(false)
})
}, [])
if (loading) return <div>Chargement...</div>
if (error) return <div>Erreur: {error.message}</div>
return (
<ul>
{produits.map(produit => (
<li key={produit.id}>{produit.nom}</li>
))}
</ul>
)
}
Les problèmes de cette approche
- Code répétitif : loading, error, data partout
- Pas de cache : on refetch à chaque render
- Pas de synchronisation : si les données changent ailleurs, on sait pas
- Gestion d'erreur basique : retry ? background refetch ? oublié !
- Performance pourrie : pas d'optimisations
Les Solutions Modernes
1. Fetch avec useState/useEffect (Basique mais OK)
// ✅ Version un peu mieux organisée
function useApi(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true)
const response = await fetch(url)
if (!response.ok) throw new Error('Erreur réseau')
const result = await response.json()
setData(result)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
fetchData()
}, [url])
return { data, loading, error }
}
// Utilisation
function ProduitsList() {
const { data: produits, loading, error } = useApi('/api/produits')
if (loading) return <div>Chargement...</div>
if (error) return <div>Erreur: {error}</div>
return (
<ul>
{produits?.map(produit => (
<li key={produit.id}>{produit.nom}</li>
))}
</ul>
)
}
2. TanStack Query (Ex-React Query) - LE BOSS ! 🔥
npm install @tanstack/react-query
Setup dans ton app :
// main.jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<MonApp />
</QueryClientProvider>
)
}
Utilisation super simple :
import { useQuery } from '@tanstack/react-query'
function ProduitsList() {
const { data: produits, isLoading, error } = useQuery({
queryKey: ['produits'],
queryFn: () => fetch('/api/produits').then(res => res.json()),
staleTime: 5 * 60 * 1000, // 5 minutes
})
if (isLoading) return <div>Chargement...</div>
if (error) return <div>Erreur: {error.message}</div>
return (
<ul>
{produits?.map(produit => (
<li key={produit.id}>{produit.nom}</li>
))}
</ul>
)
}
3. Mutations avec TanStack Query
import { useMutation, useQueryClient } from '@tanstack/react-query'
function AjouterProduit() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (nouveauProduit) =>
fetch('/api/produits', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(nouveauProduit)
}),
onSuccess: () => {
// Invalidate et refetch
queryClient.invalidateQueries(['produits'])
},
})
const handleSubmit = (e) => {
e.preventDefault()
const formData = new FormData(e.target)
mutation.mutate({
nom: formData.get('nom'),
prix: formData.get('prix')
})
}
return (
<form onSubmit={handleSubmit}>
<input name="nom" placeholder="Nom du produit" />
<input name="prix" type="number" placeholder="Prix" />
<button type="submit" disabled={mutation.isLoading}>
{mutation.isLoading ? 'Ajout...' : 'Ajouter'}
</button>
</form>
)
}
Exemples Pratiques Avancés
Custom Hook pour API avec Auth
function useAuthenticatedApi(url) {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['api', url],
queryFn: async () => {
const token = localStorage.getItem('authToken')
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
})
if (response.status === 401) {
// Redirect to login
window.location.href = '/login'
return
}
if (!response.ok) {
throw new Error(`Erreur ${response.status}: ${response.statusText}`)
}
return response.json()
},
retry: (failureCount, error) => {
// Pas de retry pour les erreurs 401, 403, 404
if ([401, 403, 404].includes(error.status)) return false
return failureCount < 3
},
staleTime: 2 * 60 * 1000, // 2 minutes
})
return { data, isLoading, error, refetch }
}
Gestion d'état Global avec API
// hooks/useUser.js
function useUser() {
return useQuery({
queryKey: ['user'],
queryFn: () => fetch('/api/user').then(res => res.json()),
staleTime: 10 * 60 * 1000, // 10 minutes
cacheTime: 15 * 60 * 1000, // 15 minutes
})
}
// hooks/useUpdateUser.js
function useUpdateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (userData) =>
fetch('/api/user', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
}),
onSuccess: (data) => {
// Update le cache directement
queryClient.setQueryData(['user'], data)
},
})
}
// Utilisation dans un composant
function ProfilUtilisateur() {
const { data: user, isLoading } = useUser()
const updateUser = useUpdateUser()
const handleSave = async (formData) => {
try {
await updateUser.mutateAsync(formData)
alert('Profil mis à jour!')
} catch (error) {
alert('Erreur lors de la mise à jour')
}
}
if (isLoading) return <div>Chargement du profil...</div>
return (
<div>
<h2>Profil de {user?.name}</h2>
{/* Formulaire ici */}
</div>
)
}
Optimistic Updates
function useLikeProduit() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ produitId, liked }) =>
fetch(`/api/produits/${produitId}/like`, {
method: 'POST',
body: JSON.stringify({ liked })
}),
onMutate: async ({ produitId, liked }) => {
// Cancel les requêtes en cours
await queryClient.cancelQueries(['produits'])
// Snapshot de l'état actuel
const previousProduits = queryClient.getQueryData(['produits'])
// Update optimiste
queryClient.setQueryData(['produits'], old =>
old?.map(p =>
p.id === produitId
? { ...p, liked, likes: p.likes + (liked ? 1 : -1) }
: p
)
)
return { previousProduits }
},
onError: (err, variables, context) => {
// Rollback en cas d'erreur
queryClient.setQueryData(['produits'], context.previousProduits)
},
onSettled: () => {
// Refetch pour être sûr
queryClient.invalidateQueries(['produits'])
},
})
}
Configuration Avancée de TanStack Query
// queryClient.js
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
// 5 minutes de stale time par défaut
staleTime: 5 * 60 * 1000,
// 10 minutes de cache time
cacheTime: 10 * 60 * 1000,
// 3 retry par défaut
retry: 3,
// Refetch quand on refocus la fenêtre
refetchOnWindowFocus: true,
// Refetch quand on reconnecte
refetchOnReconnect: true,
},
mutations: {
// 3 retry pour les mutations aussi
retry: 3,
},
},
})
DevTools pour le Debug
// En développement seulement
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
function App() {
return (
<QueryClientProvider client={queryClient}>
<MonApp />
{process.env.NODE_ENV === 'development' && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</QueryClientProvider>
)
}
Alternatives à TanStack Query
SWR (Simple mais efficace)
import useSWR from 'swr'
const fetcher = (url) => fetch(url).then(res => res.json())
function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher)
if (error) return <div>Failed to load</div>
if (isLoading) return <div>Loading...</div>
return <div>Hello {data.name}!</div>
}
Apollo Client (Pour GraphQL)
Si tu fais du GraphQL, Apollo Client est LE truc à utiliser.
import { useQuery, gql } from '@apollo/client'
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
}
}
`
function Users() {
const { loading, error, data } = useQuery(GET_USERS)
if (loading) return 'Loading...'
if (error) return `Error! ${error.message}`
return (
<ul>
{data.users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}