Comment invalider des données en cache dans Apollo et gérer la mise à jour des requêtes paginées

Après avoir passé du temps à comprendre et à mettre en œuvre une pile basée sur graphQL, je suis époustouflé par la facilité avec laquelle il est facile de développer rapidement des applications complexes. Je suis particulièrement impressionné par la manière dont les requêtes graphQL permettent aux développeurs front-end d’accéder aux données dont ils ont besoin sur plusieurs types de données connexes sans avoir besoin d’un point de terminaison spécifique pour chaque requête.

Cela dit, la pile n’est pas sans problèmes. Sur le front, l'un de ces défis concerne l'invalidation des données stockées dans le cache Apollo lorsque ces données ont été modifiées côté serveur. Cela peut être particulièrement gênant lorsque vous travaillez avec des requêtes paginées.

Notre application de départ

Tout au long de cet article, nous allons passer en revue quelques exemples d’une application Next.js simple qui utilise Apollo pour extraire des données d’un back-end graphQL Yoga / Prisma encore plus simple. Si vous souhaitez suivre ces exemples, vous pouvez récupérer les fichiers de démarrage ici, sur la branche principale: https://github.com/martinseanhunt/padgey-nation-frontend

Ce front-end est connecté au back-end de démonstration hébergé sur now.sh, vous devriez donc pouvoir exécuter npm install et npm run dev et vous serez prêt à partir!

Le problème

Lors de l’utilisation du client Apollo, la réponse de toute requête envoyée au serveur est mise en cache localement (à moins que nous n’indiquions explicitement qu’elle n’est pas celle sur laquelle nous reviendrons plus tard). Ceci est idéal pour accélérer notre application, car Apollo récupérera les données demandées dans le cache plutôt que de demander au serveur les données que nous avons déjà.

La mise en cache des données accélère les demandes répétées et réduit la charge sur le serveur

Toutefois, dès que nous modifions les données de notre serveur via une mutation graphQL, ce changement ne sera pas répercuté sur l'état de l'application à moins que nous actualisions la page.

L'ajout d'un élément nécessite une actualisation pour voir les modifications, il en va de même pour la suppression

refetchQueries

Apollo nous donne une fonction pratique appelée refetchQueries qui peut être transmise à un tableau de requêtes nécessitant une nouvelle extraction une fois que nous avons effectué une mutation. Cependant, toutes les requêtes que nous transmettons à refetchQueries seront appelées dès que la mutation sera terminée, ce qui est loin d'être idéal pour les données paginées. En supposant que 100 pages de données soient toutes renseignées dans des requêtes distinctes, l'utilisation de refetchQueries enverrait 100 requêtes réseau simultanément à notre serveur.

Les serveurs sont en feu

readQuery & writeQuery

Nous pouvons également lire et écrire des données dans le cache Apollo à l'aide des fonctions readQuery et writeQuery, qui sont, dans de nombreux cas, parfaitement adaptées à ce type d'opérations. Nous pouvons simplement insérer notre élément nouvellement créé dans ou filtrer l'élément supprimé de notre cache, en affichant instantanément les mises à jour à l'écran. Cependant, cette approche peut poser des problèmes lors du traitement de requêtes paginées en fonction des pages que l'utilisateur a mises en cache avant d'initier la mutation. Cela peut être particulièrement problématique lorsque plusieurs modifications sont apportées.

Par exemple, voici un problème que nous pouvons rencontrer lors de la suppression manuelle d’un élément du cache à l’aide de la fonction de mise à jour de notre composant Create.js:

Voici un exemple visuel de l’un des problèmes que cela peut créer:

Les articles de test 11 et 12 disparaissent jusqu'à l'actualisation

Nous rencontrons un problème similaire lorsque nous ajoutons des éléments nouvellement créés au cache de cette manière.

Les articles 22, 21 et 20 sont reproduits à la page 2

Nous nous exposons également à un problème complexe ici. Parce que nous voulons toujours ajouter les données d’un nouvel élément à la page 1, et si ces données en cache n’existent pas, nous obtenons une erreur lors de l’appel du cache.

Ici, nous avons entré l'application à la page 2 avant de créer un élément, donc aucune donnée pour skip: 0 existe dans le cache

Si vous souhaitez explorer la base de code pour cette approche, vous pouvez le trouver ici: https://github.com/martinseanhunt/padgey-nationendfront/tree/update-cache-directly

fetchPolicy

Une autre solution potentielle consiste à définir la valeur fetchPolicy de nos requêtes initiales sur «réseau uniquement». Cela signifie que les données de la page actuelle proviendront toujours du serveur. Cette approche fonctionne très bien, mais vous perdez l’impression d’être livré avec la fourniture des données en cache lorsque celles-ci sont disponibles et davantage de demandes seront envoyées au serveur que nécessaire.

  query = {GET_LIST_ITEMS_QUERY}
  fetchPolicy = 'réseau uniquement'
  variables = {{skip: (page - 1) * perPage}}
>

Où allons-nous à partir d'ici ?

L’équipe Apollo est consciente de ce problème et travaille actuellement sur une solution qui fera partie de l’API Apollo. Toutefois, si, comme moi, vous avez soif de solutions de contournement, vous avez plusieurs options!

Une solution de travail temporaire…

Heureusement, il existe une solution très simple qui devrait fonctionner dans la plupart des cas. Lorsque nous avons accès à l'objet de cache, nous pouvons appeler cache.data.delete (key) où key est la clé utilisée par Apollo pour stocker les données d'un élément spécifique. Et l'enregistrement sera entièrement supprimé du cache.

Voici le cache Apollo contenant nos deux premières pages de données de notre application

Vous pouvez voir qu'Apollo prend le nom de type de notre donnée particulière et ajoute un identifiant unique pour créer la clé.

Cela signifie que dans nos fonctions de mise à jour, nous pouvons simplement parcourir notre cache et supprimer toutes les entrées dont la clé commence par ListItem: en supprimant toutes nos requêtes paginées du cache et en forçant une nouvelle extraction lors de la prochaine demande de page.

Pour que cela fonctionne dans notre application, nous pouvons faire ce qui suit:

1) Créez une fonction d'assistance qui gérera la suppression de nos requêtes paginées du cache.

2) Mettez à jour notre mutation dans Create.js pour appeler notre nouvelle fonction d'assistance lors de la création d'un nouvel élément de liste. (n'oubliez pas de récupérer la requête de connexion aussi)

3) Dans Index.js, la récupération de la structure à partir de l’objet donné pour rendre la fonction prop sous notre requête et la transmettre à ListItem.js

Nous transmettons cette fonction de récupération à ListItems.js afin que nous puissions appeler une récupération de la page en cours lors de la suppression d'un élément. Sinon, dans la version actuelle d'Apollo, le composant Index.js n'est pas rendu et la requête ne sera pas réexécutée. Je pense que la raison pour laquelle utiliser cache.data.delete ne déclenche pas le rendu des composants parents, c’est que nous sommes en train de muter directement l’objet de données du cache, mais c’est une chose pour laquelle je ne suis pas sûr à 100%.

4) Créez et appelez notre fonction de mise à jour dans ListItems.js lorsqu’un listItem est supprimé (n’oubliez pas à nouveau de récupérer notre requête de connexion de pagination)

5) Assurez-vous de disposer de toutes les importations et exportations appropriées pour les requêtes, les fonctions d'assistance et… Succès!

Application fonctionnant comme prévu

Nous avons maintenant une application opérationnelle dans laquelle vous pouvez ajouter et supprimer des éléments et les données de page ne seront demandées au serveur qu'en cas de besoin.

La base de code complète de cette solution est disponible ici: https://github.com/martinseanhunt/padgey-nationendfront/tree/simple-solution

Vous pouvez voir la différence complète entre les fichiers de démarrage et la solution de travail ici: https://github.com/martinseanhunt/padgey-nationendfront/compare/master...simple-solution

Et vous pouvez voir une démonstration en direct sur https://padgey-nation.now.sh

5) Inconvénients de cette solution de contournement

Ne célébrons pas trop tôt…

Un problème avec cette solution de contournement est que toutes les autres données en cache portant le nom de type ListItem seront supprimées du cache local et devront être extraites même si elles ne font pas partie de nos requêtes paginées. Par exemple, si nous avions une liste secondaire d'éléments de liste «marqués» que nous n'avons pas nécessairement besoin de mettre à jour lorsqu'un élément est mis à jour ou supprimé, nous n'aurions aucun moyen de différencier les deux afin que ces requêtes soient également relues. du serveur la prochaine fois qu'ils sont interrogés.

Étant donné que nous devons mapper l'intégralité de notre cache à chaque fois qu'une entrée est ajoutée ou supprimée, je ne sais pas si cela entraînerait des problèmes de performances avec un front-end important et complexe.

L’autre problème, c’est que nous modifions notre cache local très directement, ce qui pourrait avoir des conséquences secondaires inattendues (comme nous l’avons constaté lorsqu’il fallait appeler manuellement la récupération après suppression d’un élément).

Dans l’ensemble, j'estime qu’il s’agit d’une excellente solution provisoire pour la plupart des cas d’utilisation jusqu’à ce qu’Apollo puisse offrir une meilleure solution.

Il y a une autre manière dont je suis au courant

Je suis tombé sur la solution de contournement ci-dessus quand quelqu'un l'a mentionné il y a quelques jours dans un problème de github que je suivais (https://github.com/apollographql/apollo-feature-requests/issues/4#issuecomment-431119231). Globalement, je pense que c'est la solution la plus simple et la plus élégante pour le moment. Cependant, au moment où je suis tombé sur la méthode ci-dessus, je travaillais déjà sur une solution qui n'a pas besoin de mapper sur la totalité du cache, ni de muter directement l'objet de cache. Cette méthode fonctionnera également si vous avez des éléments avec le même nom de type dans d'autres parties de votre application qui n'ont pas besoin d'être mis à jour.

Cette solution est toutefois un peu plus complexe et je pense qu’elle peut encore être améliorée.

La vue d'ensemble de haut niveau de l'idée est que nous maintenons un tableau dans l'état local des numéros de page qui doivent être mis à jour et que, selon que la page en cours interrogée se trouve ou non dans le tableau, nous envoyons une requête avec fetchPolicy de 'network- seulement 'ou' cache-first '

Voici les deux fonctions principales impliquées dans la réalisation de ce travail:

1) setPagesToBeRefreshed - cela est appelé lors de la mise à jour par nos mutations responsables de la création et de la suppression d'éléments de liste

2) getItemsForPage - Ceci est appelé à différents moments du cycle de vie d'Index.js pour déterminer s'il faut rechercher nos données dans le cache ou aller directement au serveur.

Je n’entrerai pas dans les détails ici car je pense que la première méthode est l’option la plus simple et la plus applicable dans la plupart des cas. Cependant, s'il y a une demande, je me ferai un plaisir d'écrire une suite à celle-ci et d'expliquer la deuxième solution plus en détail.

Si vous êtes curieux, vous pouvez explorer la base de code complète ici: https://github.com/martinseanhunt/padgey-nationend/tree/refetch-queries-only-whenneeded

La différence entre les fichiers de départ et le produit final est visible ici: https://github.com/martinseanhunt/padgey-nationendfront/compare/master...refetch-queries-only-when-newededed

Emballer

Comprendre tout cela a été un sacré voyage. Je ne connais pas encore l’écosystème graphQL, mais je suis extrêmement heureux de voir où ces technologies nous mèneront. Si quelqu'un a des critiques constructives ou des commentaires sur cet article, j'aimerais bien l'entendre.

Je suis impatient de trouver une solution officielle à ce problème, mais dans l’intervalle, j’espère vraiment que cela pourra être utile à quelqu'un.

Bonne codage!