Normy
Automatic normalization and data updates for data fetching libraries (react-query, vue-query, trpc, swr, rtk-query and more)
Install / Use
/learn @klis87/NormyREADME
Automatic normalization and data updates for data fetching libraries (react-query, swr, rtk-query and more)
Table of content
- Introduction
- Motivation
- Installation
- Required conditions
- Normalization of arrays
- Debugging
- Performance
- Integrations
- Examples
Introduction :arrow_up:
normy is a library, which allows your application data to be normalized automatically. Then, once data is normalized, in many cases your data can be updated automatically.
The core of normy - namely @normy/core library, which is not meant to be used directly in applications, has logic inside which allows an easily integration with your favourite data fetching libraries. There are already official integrations with react-query, swr and RTK Query. If you use another fetching library, you could raise the Github issue, so it might be added
as well.
Motivation :arrow_up:
In order to understand what normy actually does, it is the best to see an example. Let's assume you use react-query. Then you could refactor a code in the following way:
import React from 'react';
import {
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({
books: [
{ 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',
author: { id: '1001', name: 'User1' },
}),
);
const updateBookNameMutation = useMutation({
mutationFn: () => ({
id: '1',
name: 'Name 1 Updated',
}),
- onSuccess: mutationData => {
- queryClient.setQueryData(['books'], data => ({
- books: data.books.map(book =>
- book.id === mutationData.id ? { ...book, ...mutationData } : book,
- ),
- }));
- queryClient.setQueryData(['book'], data =>
- data.id === mutationData.id ? { ...data, ...mutationData } : data,
- );
- },
});
const updateBookAuthorMutation = useMutation({
mutationFn: () => ({
id: '1',
author: { id: '1004', name: 'User4' },
}),
- onSuccess: mutationData => {
- queryClient.setQueryData(['books'], data => ({
- books: data.books.map(book =>
- book.id === mutationData.id ? { ...book, ...mutationData } : book,
- ),
- }));
- queryClient.setQueryData(['book'], data =>
- data.id === mutationData.id ? { ...data, ...mutationData } : data,
- );
- },
});
const addBookMutation = useMutation({
mutationFn: () => ({
id: '3',
name: 'Name 3',
author: { id: '1003', name: 'User3' },
+ __append: 'books',
}),
- onSuccess: mutationData => {
- queryClient.setQueryData(
- ['books'],
- data => ({ books: data.books.concat(mutationData) }),
- );
- },
});
// return some JSX
};
const App = () => (
+ <QueryNormalizerProvider
+ queryClient={queryClient}
+ normalizerConfig={{ getArrayType: ({ arrayKey }) => arrayKey }}
+ >
<QueryClientProvider client={queryClient}>
<Books />
</QueryClientProvider>
+ </QueryNormalizerProvider>
);
So, as you can see, no manual data updates are necessary anymore. This is especially handy if a given mutation
should update data for multiple queries. Not only this is verbose to do updates manually, but also you need to exactly know,
which queries to update. The more queries you have, the bigger advantages normy brings.
How does it work? By default all objects with id key are
organized by their ids. Now, any object with key id
will be normalized, which simply means stored by id. If there is already a matching object
with the same id, a new one will be deeply merged with the one already in the state.
So, if a server response data from a mutation is { id: '1', title: 'new title' },
this library will automatically figure it out to update title for object with id: '1' for all dependent queries.
It also works with nested objects with ids, no matter how deep. If an object with id has other objects with ids, then those will be normalized separately and parent object will have just reference to those nested objects.
Notice, that it even works for array operations! It requires though 2 things first, config callback to compute array types getArrayType
and some hints, what operations should be applied (in our case `__append: 'books'). We will cover this more in arrays chapter,
but to give you some teaser, it is very flexible, we have numerous built-in operations, plus it is possible to create custom array operations. All of this should prevent any need to write any imperative code to update any data, no exceptions!
Installation :arrow_up:
react-query
To install the package, just run:
$ npm install @normy/react-query
or you can just use CDN: https://unpkg.com/@normy/react-query.
vue-query
To install the package, just run:
$ npm install @normy/vue-query
or you can just use CDN: https://unpkg.com/@normy/vue-query.
swr
To install the package, just run:
$ npm install @normy/swr
or you can just use CDN: https://unpkg.com/@normy/swr.
rtk-query
To install the package, just run:
$ npm install @normy/rtk-query
or you can just use CDN: https://unpkg.com/@normy/rtk-query.
another lirary
If you want to write a plugin to another library than react-query, swr or rtk-query:
$ npm install @normy/core
or you can just use CDN: https://unpkg.com/@normy/core.
To see how to write a plugin, for now just check source code of @normy/react-query, it is very easy to do,
in the future a guide will be created.
Required conditions :arrow_up:
In order to make automatic normalization work, the following conditions must be met:
- you must have a standardized way to identify your objects, usually this is done by key
id - ids must be unique across the whole app, not only across object types, if not, you will need to append something to them,
the same has to be done in GraphQL world, usually adding
_typename - objects with the same ids should have a consistent structure, if an object like book in one
query has
titlekey, it should betitlein others, notnameout of a sudden
There is a function which can be passed to createQueryNormalizer to meet those requirements, namely getNormalizationObjectKey.
getNormalizationObjectKey can help you with 1st point, if for instance you identify
objects differently, like by _id key, then you can pass getNormalizationObjectKey: obj => obj._id.
getNormalizationObjectKey also allows you to pass the 2nd requirement. For example, if your ids
are unique, but not across the whole app, but within object types, you could use
getNormalizationObjectKey: obj => obj.id && obj.type ? obj.id + obj.type : undefined or something similar.
If that is not possible, then you could just compute a suffix yourself, for example:
const getType = obj => {
if (obj.bookTitle) {
return 'book';
}
if (obj.surname) {
return 'user';
}
return undefined;
};
createQueryNormalizer(queryClient, {
getNormalizationObjectKey: obj =>
obj.id && getType(obj) && obj.id + getType(obj),
});
Point 3 should always be met, if not, your really should ask your backend developers to keep things standardized and consistent. As a last resort, you can amend responses on your side.
Normalization of arrays :arrow_up:
Let's say you have a data in a query like:
{
id: '1',
name: 'Book name',
authors: [{ id: '2', name: 'Author name' }],
}
Then, updating authors is easy, just have a mutation with response:
{
id: '1',
authors: [{ id: '2', name: 'Author name 2' }, { id: '3', name: 'Author name 3' }],
}
Then, just authors array will be replaced with the new one. But what if you have some data with arrays not belonging to any object? Like:
{
books: [{ id: '1', name: 'Book name' }],
}
For such cases, typically you would need to either refetch such a query or... update books data m
Related Skills
bluebubbles
338.7kUse when you need to send or manage iMessages via BlueBubbles (recommended iMessage integration). Calls go through the generic message tool with channel="bluebubbles".
node-connect
338.7kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
slack
338.7kUse when you need to control Slack from OpenClaw via the slack tool, including reacting to messages or pinning/unpinning items in Slack channels or DMs.
frontend-design
83.6kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
