Automatische Normalisierung und Datenaktualisierungen für Datenabrufbibliotheken (React-Query, SWR, RTK-Query und mehr)
Einführung
Motivation
Installation
Erforderliche Bedingungen
Normalisierung von Arrays
Debuggen
Leistung
Integrationen
Beispiele
normy
ist eine Bibliothek, die es ermöglicht, Ihre Anwendungsdaten automatisch zu normalisieren. Sobald die Daten dann normalisiert sind, können Ihre Daten in vielen Fällen automatisch aktualisiert werden.
Der Kern von normy
– nämlich @normy/core
-Bibliothek, die nicht für die direkte Verwendung in Anwendungen gedacht ist – enthält eine Logik, die eine einfache Integration mit Ihren bevorzugten Datenabrufbibliotheken ermöglicht. Es gibt bereits offizielle Integrationen mit react-query
, swr
und RTK Query
. Wenn Sie eine andere Abrufbibliothek verwenden, könnte das Github-Problem auftreten, sodass diese möglicherweise ebenfalls hinzugefügt wird.
Um zu verstehen, was normy
eigentlich macht, ist es am besten, sich ein Beispiel anzusehen. Nehmen wir an, Sie verwenden react-query
. Dann könnten Sie einen Code folgendermaßen umgestalten:
import React aus 'react'; importieren { QueryClientProvider, QueryClient, useQueryClient, } from '@tanstack/react-query';+ import { QueryNormalizerProvider } from '@normy/react-query'; const queryClient = new QueryClient(); const Books = () => { const queryClient = useQueryClient(); const { data: BooksData = [] } = useQuery(['books'], () => Promise.resolve([ { id: '1', name: 'Name 1', author: { id: '1001', name: 'User1' } }, { id: '2', name: 'Name 2', author: { id: '1002', name: 'User2' } }, ]), ); const { data: bookData } = useQuery(['book'], () => Promise.resolve({ ID: '1', Name: 'Name 1', Autor: { ID: '1001', Name: 'Benutzer1' }, }), ); const updateBookNameMutation = useMutation({ mutationFn: () => ({ ID: '1', name: 'Name 1 aktualisiert', }),- onSuccess: mutationData => {- queryClient.setQueryData(['books'], data =>- data.map(book =>- book.id === mutationData.id ? { ...book, . ..mutationData } : book,- ),- );- queryClient.setQueryData(['book'], data =>- data.id === mutationData.id ? { ...data, ...mutationData } : Daten,- );- },}); const updateBookAuthorMutation = useMutation({ mutationFn: () => ({ ID: '1', Autor: { id: '1004', name: 'User4' }, }),- onSuccess: mutationData => {- queryClient.setQueryData(['books'], data =>- data.map(book =>- book.id === mutationData.id ? { ...book, . ..mutationData } : book,- ),- );- queryClient.setQueryData(['book'], data =>- data.id === mutationData.id ? { ...data, ...mutationData } : Daten,- );- },}); const addBookMutation = useMutation({ mutationFn: () => ({ ID: '3', Name: 'Name 3', Autor: { id: '1003', name: 'User3' }, }), // Bei Daten mit Arrays der obersten Ebene müssen Sie die Daten weiterhin manuell aktualisieren onSuccess: mutationData => { queryClient.setQueryData(['books'], data => data.concat(mutationData)); }, }); // etwas JSX zurückgeben }; const App = () => (+ <QueryNormalizerProvider queryClient={queryClient}> <QueryClientProvider client={queryClient}> <Bücher /> </QueryClientProvider>+ </QueryNormalizerProvider> );
Wie Sie sehen, sind abgesehen von Arrays der obersten Ebene keine manuellen Datenaktualisierungen mehr erforderlich. Dies ist besonders praktisch, wenn eine bestimmte Mutation Daten für mehrere Abfragen aktualisieren soll. Das manuelle Durchführen von Aktualisierungen ist nicht nur sehr ausführlich, sondern Sie müssen auch genau wissen, welche Abfragen aktualisiert werden müssen. Je mehr Anfragen Sie haben, desto größere Vorteile bringt normy
.
Wie funktioniert es? Standardmäßig werden alle Objekte mit id
-Schlüssel nach ihren IDs organisiert. Jetzt wird jedes Objekt mit der Schlüssel id
normalisiert, was einfach bedeutet, dass es nach der ID gespeichert wird. Wenn es bereits ein passendes Objekt mit derselben ID gibt, wird ein neues Objekt tief mit dem Objekt zusammengeführt, das sich bereits im Status befindet. Wenn also die Antwortdaten eines Servers aus einer Mutation { id: '1', title: 'new title' }
lauten, erkennt diese Bibliothek automatisch, dass title
für das Objekt mit id: '1'
für alle abhängigen Abfragen aktualisiert wird.
Es funktioniert auch mit verschachtelten Objekten mit IDs, egal wie tief. Wenn ein Objekt mit ID andere Objekte mit IDs hat, werden diese separat normalisiert und das übergeordnete Objekt verweist nur auf diese verschachtelten Objekte.
Um das Paket zu installieren, führen Sie einfach Folgendes aus:
$ npm install @normy/react-query
oder Sie können einfach CDN verwenden: https://unpkg.com/@normy/react-query
.
Um das Paket zu installieren, führen Sie einfach Folgendes aus:
$ npm install @normy/swr
oder Sie können einfach CDN verwenden: https://unpkg.com/@normy/swr
.
Um das Paket zu installieren, führen Sie einfach Folgendes aus:
$ npm install @normy/rtk-query
oder Sie können einfach CDN verwenden: https://unpkg.com/@normy/rtk-query
.
Wenn Sie ein Plugin in eine andere Bibliothek als react-query
, swr
oder rtk-query
schreiben möchten:
$ npm install @normy/core
oder Sie können einfach CDN verwenden: https://unpkg.com/@normy/core
.
Um zu sehen, wie man ein Plugin schreibt, überprüfen Sie zunächst einfach den Quellcode von @normy/react-query
. Das ist sehr einfach. In Zukunft wird eine Anleitung erstellt.
Damit die automatische Normalisierung funktioniert, müssen die folgenden Bedingungen erfüllt sein:
Sie benötigen eine standardisierte Methode zur Identifizierung Ihrer Objekte. Normalerweise erfolgt dies anhand der Schlüssel id
IDs müssen in der gesamten App eindeutig sein, nicht nur zwischen Objekttypen. Andernfalls müssen Sie etwas an sie anhängen. Dasselbe muss in der GraphQL-Welt erfolgen, indem normalerweise _typename
hinzugefügt wird
Objekte mit denselben IDs sollten eine konsistente Struktur haben. Wenn ein Objekt wie ein Buch in einer Abfrage einen title
hat, sollte es in anderen title
sein und nicht plötzlich name
Es gibt eine Funktion, die an createQueryNormalizer
übergeben werden kann, um diese Anforderungen zu erfüllen, nämlich getNormalizationObjectKey
.
getNormalizationObjectKey
kann Ihnen beim ersten Punkt helfen. Wenn Sie beispielsweise Objekte anders identifizieren, beispielsweise anhand _id
-Schlüssels, können Sie getNormalizationObjectKey: obj => obj._id
.
getNormalizationObjectKey
können Sie auch die zweite Anforderung erfüllen. Wenn Ihre IDs beispielsweise eindeutig sind, jedoch nicht in der gesamten App, sondern innerhalb von Objekttypen, können Sie getNormalizationObjectKey: obj => obj.id && obj.type ? obj.id + obj.type : undefined
oder etwas Ähnliches. Wenn das nicht möglich ist, können Sie einfach selbst ein Suffix berechnen, zum Beispiel:
const getType = obj => { if (obj.bookTitle) {return 'book'; } if (obj.surname) {return 'user'; } return undefiniert;};createQueryNormalizer(queryClient, { getNormalizationObjectKey: obj =>obj.id && getType(obj) && obj.id + getType(obj),});
Punkt 3 sollte immer erfüllt sein. Wenn nicht, sollten Sie Ihre Backend-Entwickler wirklich bitten, die Dinge standardisiert und konsistent zu halten. Als letzten Ausweg können Sie die Antworten Ihrerseits ändern.
Leider bedeutet das nicht, dass Sie die Daten nie mehr manuell aktualisieren müssen. Einige Aktualisierungen müssen immer noch wie üblich manuell durchgeführt werden, nämlich das Hinzufügen und Entfernen von Elementen zum Array. Warum? Stellen Sie sich eine REMOVE_BOOK
Mutation vor. Dieses Buch könnte in vielen Abfragen vorhanden sein. Die Bibliothek kann nicht wissen, aus welchen Abfragen Sie es entfernen möchten. Das Gleiche gilt für ADD_BOOK
. Die Bibliothek kann nicht wissen, zu welcher Abfrage ein Buch hinzugefügt werden soll oder welchen Array-Index es hat. Das Gleiche gilt für Aktionen wie SORT_BOOKS
. Dieses Problem betrifft jedoch nur Arrays der obersten Ebene. Wenn Sie beispielsweise ein Buch mit einer ID und einem anderen Schlüssel wie likedByUsers
haben und dann ein neues Buch mit einer aktualisierten Liste in likedByUsers
zurückgeben, funktioniert dies automatisch wieder.
In der zukünftigen Version der Bibliothek wird es jedoch mit einigen zusätzlichen Hinweisen möglich sein, die oben genannten Aktualisierungen durchzuführen!
Wenn Sie daran interessiert sind, welche Datenmanipulationen normy
tatsächlich durchführt, können Sie die Option devLogging
verwenden:
<QueryNormalizerProvider queryClient={queryClient} normalizerConfig={{ devLogging: true }}> {children}</QueryNormalizerProvider>
standardmäßig false
, wenn auf true
gesetzt, können Sie in den Konsoleninformationen sehen, wann Abfragen festgelegt oder entfernt werden.
Beachten Sie, dass dies nur in der Entwicklung funktioniert. Selbst wenn Sie true
übergeben, erfolgt in der Produktion keine Protokollierung (wenn genau: process.env.NODE_ENV === 'production'
). NODE_ENV
wird normalerweise von Modul-Bundlern wie webpack
für Sie festgelegt, sodass Sie sich wahrscheinlich nicht darum kümmern müssen, NODE_ENV
selbst festzulegen.
Wie immer ist jede Automatisierung mit Kosten verbunden. In Zukunft könnten einige Benchmarks hinzugefügt werden, aber vorerst haben manuelle Tests gezeigt, dass der Mehraufwand nicht spürbar sein sollte, es sei denn, Ihre Daten enthalten Zehntausende normalisierter Objekte. Sie haben jedoch mehrere flexible Möglichkeiten, die Leistung zu verbessern:
Sie können nur Abfragen normalisieren, die über Datenaktualisierungen verfügen, und nur Mutationen, die Daten aktualisieren sollen – das ist alles, Sie können nur einen Teil Ihrer Daten normalisieren lassen. Schauen Sie in der Integrationsdokumentation nach, wie das geht.
Wie 1.
, jedoch für Abfragen und Mutationen mit extrem großen Datenmengen.
Es gibt eine integrierte Optimierung, die Daten aus Mutationsantworten prüft, ob sie sich tatsächlich von den Daten im normalisierten Speicher unterscheiden. Wenn es identisch ist, werden abhängige Abfragen nicht aktualisiert. Daher ist es sinnvoll, dass Mutationsdaten nur Dinge enthalten, die tatsächlich unterschiedlich sein könnten, um unnötige Normalisierungen und Abfrageaktualisierungen zu vermeiden.
Deaktivieren Sie die Option structuralSharing
nicht in Bibliotheken, die sie unterstützen. Wenn die Abfragedaten nach der Aktualisierung referenziell dieselben sind wie vor der Aktualisierung, wird diese Abfrage nicht normalisiert. Dies stellt eine große Leistungsoptimierung dar, insbesondere nach einem erneuten Abruf bei erneuter Fokussierung, wodurch mehrere Abfragen gleichzeitig aktualisiert werden könnten, normalerweise auf genau dieselben Daten.
Mit der Funktion getNormalizationObjectKey
können Sie global festlegen, welche Objekte tatsächlich normalisiert werden sollen. Zum Beispiel:
<QueryNormalizerProvider queryClient={queryClient} normalizerConfig={{getNormalizationObjectKey: obj => (obj.normalizable ? obj.id : undefiniert), }}> {children}</QueryNormalizerProvider>
Darüber hinaus werden in Zukunft einige zusätzliche leistungsspezifische Optionen hinzugefügt.
Derzeit gibt es drei offizielle Integrationen mit Datenabrufbibliotheken, nämlich react-query
, swr
und rtk-query
. Weitere Informationen zu bestimmten Integrationen finden Sie in den entsprechenden Dokumentationen:
Reaktionsabfrage
swr
RTK-Abfrage
Ich empfehle dringend, Beispiele auszuprobieren, wie dieses Paket in realen Anwendungen verwendet werden könnte.
Derzeit gibt es folgende Beispiele:
Reaktionsabfrage
trpc
swr
RTK-Abfrage
MIT