Aller au contenu

Réagir à tRPC

Le plugin Nx pour AWS fournit un générateur pour intégrer rapidement votre API tRPC avec un site React. Il configure tous les éléments nécessaires pour connecter vos backends tRPC, incluant le support d’authentification AWS IAM et Cognito ainsi qu’une gestion d’erreurs appropriée. Cette intégration offre une sécurité de type end-to-end complète entre votre frontend et vos backends tRPC.

Avant d’utiliser ce générateur, assurez-vous que votre application React possède :

  1. Un fichier main.tsx qui rend votre application
  2. Un élément JSX <App/> où le fournisseur tRPC sera injecté automatiquement
  3. Une API tRPC fonctionnelle (générée via le générateur d’API tRPC)
  4. Une authentification Cognito ajoutée via le générateur ts#react-website-auth si vous connectez une API utilisant l’authentification Cognito ou IAM
Exemple de structure requise pour main.tsx
import { StrictMode } from 'react';
import * as ReactDOM from 'react-dom/client';
import App from './app/app';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement,
);
root.render(
<StrictMode>
<App />
</StrictMode>,
);
  1. Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
  2. Ouvrez la console Nx dans VSCode
  3. Cliquez sur Generate (UI) dans la section "Common Nx Commands"
  4. Recherchez @aws/nx-plugin - connection
  5. Remplissez les paramètres requis
    • Cliquez sur Generate
    Paramètre Type Par défaut Description
    sourceProject Requis string - The source project
    targetProject Requis string - The target project to connect to

    Le générateur crée la structure suivante dans votre application React :

    • Répertoiresrc
      • Répertoirecomponents
        • <ApiName>ClientProvider.tsx Configure les clients tRPC et les liaisons avec vos schémas backend. ApiName correspondra au nom de l’API
        • QueryClientProvider.tsx Fournisseur du client TanStack React Query
      • Répertoirehooks
        • useSigV4.tsx Hook pour signer les requêtes HTTP avec SigV4 (IAM uniquement)
        • use<ApiName>.tsx Hook retournant le proxy d’options tRPC pour l’intégration TanStack Query
        • use<ApiName>Client.tsx Hook retournant le client tRPC vanilla pour les appels d’API directs

    De plus, il installe les dépendances requises :

    • @trpc/client
    • @trpc/tanstack-react-query
    • @tanstack/react-query
    • aws4fetch (si utilisation de l’authentification IAM)
    • event-source-polyfill (si utilisation de l’API REST, pour le support des souscriptions)

    Le générateur fournit un hook use<ApiName> qui retourne un proxy d’options tRPC pour une utilisation avec les hooks TanStack Query comme useQuery et useMutation :

    import { useQuery, useMutation } from '@tanstack/react-query';
    import { useMyApi } from './hooks/useMyApi';
    function MyComponent() {
    const trpc = useMyApi();
    // Exemple de requête
    const { data, isLoading, error } = useQuery(trpc.users.list.queryOptions());
    // Exemple de mutation
    const mutation = useMutation(trpc.users.create.mutationOptions());
    const handleCreate = () => {
    mutation.mutate({
    name: 'John Doe',
    email: 'john@example.com',
    });
    };
    if (isLoading) return <div>Chargement...</div>;
    return (
    <ul>
    {data.map((user) => (
    <li key={user.id}>{user.name}</li>
    ))}
    </ul>
    );
    }

    Le hook use<ApiName>Client fournit un accès au client tRPC vanilla, utile pour les appels d’API impératifs et les souscriptions :

    import { useState } from 'react';
    import { useMyApiClient } from './hooks/useMyApi';
    function MyComponent() {
    const client = useMyApiClient();
    const handleClick = async () => {
    const result = await client.echo.query({ message: 'Hello!' });
    console.log(result);
    const mutationResult = await client.users.create.mutate({ name: 'Jane' });
    console.log(mutationResult);
    };
    return <button onClick={handleClick}>Call API</button>;
    }

    L’intégration inclut une gestion d’erreurs intégrée qui traite correctement les erreurs tRPC :

    function MyComponent() {
    const trpc = useMyApi();
    const { data, error } = useQuery(trpc.users.list.queryOptions());
    if (error) {
    return (
    <div>
    <h2>Erreur survenue :</h2>
    <p>{error.message}</p>
    {error.data?.code && <p>Code : {error.data.code}</p>}
    </div>
    );
    }
    return (
    <ul>
    {data.map((user) => (
    <li key={user.id}>{user.name}</li>
    ))}
    </ul>
    );
    }

    Lors de la connexion à un backend tRPC avec API REST, le client généré est automatiquement configuré avec un splitLink qui route les opérations de souscription via httpSubscriptionLink (utilisant SSE) et les requêtes/mutations standard via httpLink. Cela signifie que les souscriptions fonctionnent immédiatement sans configuration supplémentaire.

    Pour plus d’informations sur la définition de procédures de souscription dans votre backend, consultez le guide du générateur ts#trpc-api.

    Vous pouvez consommer des souscriptions en utilisant le hook useSubscription avec subscriptionOptions du proxy d’options :

    import { useSubscription } from '@trpc/tanstack-react-query';
    import { useMyApi } from './hooks/useMyApi';
    function StreamingComponent() {
    const trpc = useMyApi();
    const subscription = useSubscription(
    trpc.myStream.subscriptionOptions(
    { query: 'hello' },
    {
    enabled: true,
    onStarted: () => {
    console.log('Subscription started');
    },
    onData: (data) => {
    console.log('Received:', data.text);
    },
    onError: (error) => {
    console.error('Subscription error:', error);
    },
    },
    ),
    );
    return (
    <div>
    <p>Status: {subscription.status}</p>
    {subscription.data && <p>Latest: {subscription.data.text}</p>}
    {subscription.error && <p>Error: {subscription.error.message}</p>}
    <button onClick={() => subscription.reset()}>Reset</button>
    </div>
    );
    }

    L’objet subscription fournit :

    • subscription.data — les données les plus récemment reçues
    • subscription.error — l’erreur la plus récemment reçue
    • subscription.status — l’un de 'idle', 'connecting', 'pending' ou 'error'
    • subscription.reset() — réinitialise la souscription (utile pour récupérer après des erreurs)

    Alternativement, vous pouvez utiliser le client tRPC vanilla via le hook use<ApiName>Client pour plus de contrôle sur le cycle de vie de la souscription :

    import { useState, useEffect } from 'react';
    import { useMyApiClient } from './hooks/useMyApi';
    function StreamingComponent() {
    const client = useMyApiClient();
    const [messages, setMessages] = useState<string[]>([]);
    useEffect(() => {
    const subscription = client.myStream.subscribe(
    { query: 'hello' },
    {
    onData: (data) => {
    setMessages((prev) => [...prev, data.text]);
    },
    onComplete: () => {
    console.log('Stream complete');
    },
    onError: (error) => {
    console.error('Stream error:', error);
    },
    },
    );
    // Nettoyer la souscription lors du démontage
    return () => subscription.unsubscribe();
    }, [client]);
    return (
    <ul>
    {messages.map((msg, i) => (
    <li key={i}>{msg}</li>
    ))}
    </ul>
    );
    }

    Toujours gérer les états de chargement et d’erreur pour une meilleure expérience utilisateur :

    function UserList() {
    const trpc = useMyApi();
    const users = useQuery(trpc.users.list.queryOptions());
    if (users.isLoading) {
    return <LoadingSpinner />;
    }
    if (users.error) {
    return <ErrorMessage error={users.error} />;
    }
    return (
    <ul>
    {users.data.map((user) => (
    <li key={user.id}>{user.name}</li>
    ))}
    </ul>
    );
    }

    Utilisez les mises à jour optimistes pour améliorer l’expérience utilisateur :

    import { useQueryClient, useQuery, useMutation } from '@tanstack/react-query';
    function UserList() {
    const trpc = useMyApi();
    const users = useQuery(trpc.users.list.queryOptions());
    const queryClient = useQueryClient();
    const deleteMutation = useMutation(
    trpc.users.delete.mutationOptions({
    onMutate: async (userId) => {
    // Annuler les requêtes en cours
    await queryClient.cancelQueries(trpc.users.list.queryFilter());
    // Capturer l'état actuel des données
    const previousUsers = queryClient.getQueryData(
    trpc.users.list.queryKey(),
    );
    // Suppression optimiste de l'utilisateur
    queryClient.setQueryData(trpc.users.list.queryKey(), (old) =>
    old?.filter((user) => user.id !== userId),
    );
    return { previousUsers };
    },
    onError: (err, userId, context) => {
    // Restaurer les données précédentes en cas d'erreur
    queryClient.setQueryData(
    trpc.users.list.queryKey(),
    context?.previousUsers,
    );
    },
    }),
    );
    return (
    <ul>
    {users.map((user) => (
    <li key={user.id}>
    {user.name}
    <button onClick={() => deleteMutation.mutate(user.id)}>Supprimer</button>
    </li>
    ))}
    </ul>
    );
    }

    Préchargez les données pour améliorer les performances :

    function UserList() {
    const trpc = useMyApi();
    const users = useQuery(trpc.users.list.queryOptions());
    const queryClient = useQueryClient();
    // Précharger les détails utilisateur au survol
    const prefetchUser = async (userId: string) => {
    await queryClient.prefetchQuery(trpc.users.getById.queryOptions(userId));
    };
    return (
    <ul>
    {users.map((user) => (
    <li key={user.id} onMouseEnter={() => prefetchUser(user.id)}>
    <Link to={`/users/${user.id}`}>{user.name}</Link>
    </li>
    ))}
    </ul>
    );
    }

    Gérez la pagination avec des requêtes infinies :

    function UserList() {
    const trpc = useMyApi();
    const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
    useInfiniteQuery(
    trpc.users.list.infiniteQueryOptions(
    { limit: 10 },
    {
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    },
    ),
    );
    return (
    <div>
    {data?.pages.map((page) =>
    page.users.map((user) => <UserCard key={user.id} user={user} />),
    )}
    {hasNextPage && (
    <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
    {isFetchingNextPage ? 'Chargement...' : 'Charger plus'}
    </button>
    )}
    </div>
    );
    }

    Il est important de noter que les requêtes infinies ne peuvent être utilisées que pour les procédures possédant une propriété d’entrée nommée cursor.

    L’intégration fournit une sécurité de type complète de bout en bout. Votre IDE offrira l’autocomplétion et la vérification de types pour tous vos appels d’API :

    function UserForm() {
    const trpc = useMyApi();
    // ✅ L'entrée est entièrement typée
    const createUser = trpc.users.create.useMutation();
    const handleSubmit = (data: CreateUserInput) => {
    // ✅ Erreur de type si l'entrée ne correspond pas au schéma
    createUser.mutate(data);
    };
    return <form onSubmit={handleSubmit}>{/* ... */}</form>;
    }

    Les types sont automatiquement inférés depuis les définitions de routeur et de schéma de votre backend, garantissant que toute modification de votre API est immédiatement reflétée dans votre code frontend sans nécessiter de build.

    Pour plus d’informations, consultez :