SkillAgentSearch skills...

Normy

Automatic normalization and data updates for data fetching libraries (react-query, vue-query, trpc, swr, rtk-query and more)

Install / Use

/learn @klis87/Normy

README

<div align="center"> <h1> <img src="https://raw.githubusercontent.com/klis87/normy/refs/heads/master/logo.webp" width="120px" align="center" alt="Normys logo" /> <span style="font-size: 150%; vertical-align: middle;">Normy</span> <br /> <br /> </h1> </div> <br />

npm version gzip size GitHub Workflow Status Coverage Status lerna code style: prettier

Automatic normalization and data updates for data fetching libraries (react-query, swr, rtk-query and more)

Table of content

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:

  1. you must have a standardized way to identify your objects, usually this is done by key id
  2. 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
  3. objects with the same ids should have a consistent structure, if an object like book in one query has title key, it should be title in others, not name out 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

View on GitHub
GitHub Stars601
CategoryDevelopment
Updated11h ago
Forks13

Languages

TypeScript

Security Score

100/100

Audited on Mar 27, 2026

No findings