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

Ressources Pour Aller Plus Loin