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
Automatic normalisation and data updates for data fetching libraries
Table of content
- Introduction
- Motivation
- Installation
- Required conditions
- Normalisation of arrays
- Examples
- Companion libraries
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-queryor 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/coreor 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:
- you must have a standardized way to identify your objects, usually this is just
idkey - 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 consistent structure, if an object like book in one
query has
titlekey, it should betitlein others, notnameout 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