JSPM

  • Created
  • Published
  • Downloads 4101
  • Score
    100M100P100Q126185F
  • License MIT

react-query addon for normy - Automatic normalisation and data updates for data fetching libraries

Package Exports

  • @normy/react-query
  • @normy/react-query/es/index.js
  • @normy/react-query/lib/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@normy/react-query) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

Normy

npm version gzip size lerna code style: prettier

Automatic normalisation and data updates for data fetching libraries

Table of content

Introduction ⬆️

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, be it react-query, swr, RTK Query and so on. For now only @normy/react-query exists, but there are more to come.

Motivation ⬆️

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 { createNormalizedQueryClient } from '@normy/react-query';

- const queryClient = new QueryClient();
+ const queryClient = createNormalizedQueryClient();

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',
      author: { id: '1001', name: 'User1' },
    }),
  );

  const updateBookNameMutation = useMutation({
    mutationFn: () => ({
      id: '1',
      name: 'Name 1 Updated',
    }),
-   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 } : data,
-     );
-   },
  });

  const updateBookAuthorMutation = useMutation({
    mutationFn: () => ({
      id: '1',
      author: { 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 } : data,
-     );
-   },
  });

  const addBookMutation = useMutation({
    mutationFn: () => ({
      id: '3',
      name: 'Name 3',
      author: { id: '1003', name: 'User3' },
    }),
    // with data with top level arrays, you still need to update data manually
    onSuccess: mutationData => {
      queryClient.setQueryData(['books'], data => data.concat(mutationData));
    },
  });

  // return some JSX
};

const App = () => (
  <QueryClientProvider client={queryClient}>
    <Books />
  </QueryClientProvider>
);

So, as you can see, apart from arrays, no manual data updates are necessary. 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, new one will be deeply merged with the one already in state. So, if only 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'.

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.

Installation ⬆️

To install the package, just run:

$ npm install @normy/react-query

or you can just use CDN: https://unpkg.com/@normy/react-query.

If you want to write a plugin to another library than react-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 ⬆️

In order to make automatic normalisation work, the following conditions must be meet:

  1. you must have a standardized way to identify your objects, usually this is just id key
  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 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

Two functions which can be passed to createNormalizedQueryClient can help to meet those requirements, shouldObjectBeNormalized and getNormalisationObjectKey.

shouldObjectBeNormalized can help you with 1st point, if for instance you identify objects differently, for instance by _id key, then you can pass shouldObjectBeNormalized: obj => obj._id !== undefined to handleRequest.

getNormalisationObjectKey allows you to pass 2nd requirement. For example, if your ids are unique, but not across the whole app, but within object types, you could use getNormalisationObjectKey: obj => obj.id + obj.type 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';
  }

  throw 'we support only book and user object';
};

const queryClient = createNormalizedQueryClient(reactQueryConfig, {
  getNormalisationObjectKey: 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 response on your side

Normalisation of arrays ⬆️

Unfortunately it does not mean you will never need to update data manually anymore. Some updates still need to be done manually like usually, namely adding and removing items from array. Why? Imagine REMOVE_BOOK mutation. This book could be present in many queries, library cannot know from which query you would like to remove it. The same applies for ADD_BOOK, library cannot know to which query a book should be added, or even as which array index. The same thing for action like SORT_BOOKS. This problem affects only top level arrays though. For instance, if you have a book with some id and another key like likedByUsers, then if you return new book with updated list in likedByUsers, this will work again automatically.

Examples ⬆️

I highly recommend to try examples how this package could be used in real applications.

There are following examples currently:

Licence ⬆️

MIT