Normalização automática e atualizações de dados para bibliotecas de busca de dados (react-query, swr, rtk-query e mais)
Introdução
Motivação
Instalação
Condições necessárias
Normalização de matrizes
Depuração
Desempenho
Integrações
Exemplos
normy
é uma biblioteca que permite que os dados do seu aplicativo sejam normalizados automaticamente. Então, depois que os dados forem normalizados, em muitos casos eles poderão ser atualizados automaticamente.
O núcleo do normy
- ou seja, a biblioteca @normy/core
, que não se destina a ser usada diretamente em aplicativos, possui uma lógica interna que permite uma fácil integração com suas bibliotecas favoritas de busca de dados. Já existem integrações oficiais com react-query
, swr
e RTK Query
. Se você usar outra biblioteca de busca, poderá levantar o problema do Github, portanto ela também poderá ser adicionada.
Para entender o que normy
realmente faz, é melhor ver um exemplo. Vamos supor que você use react-query
. Então você poderia refatorar um código da seguinte maneira:
importar React de 'react'; importar { QueryClientProvider, ConsultaCliente, useQueryClient, } de '@tanstack/react-query';+ importar { QueryNormalizerProvider } de '@normy/react-query'; const queryClient = new QueryClient(); const Livros = () => { const queryClient = useQueryClient(); const { dados: livrosData = [] } = useQuery(['livros'], () => Promessa.resolver([ { id: '1', nome: 'Nome 1', autor: { id: '1001', nome: 'Usuário1' } }, { id: '2', nome: 'Nome 2', autor: { id: '1002', nome: 'Usuário2' } }, ]), ); const {dados:bookData} = useQuery(['livro'], () => Promessa.resolver({ identificação: '1', nome: 'Nome 1', autor: {id: '1001', nome: 'Usuário1' }, }), ); const updateBookNameMutation = useMutation({ mutaçãoFn: () => ({ identificação: '1', nome: 'Nome 1 atualizado', }),- onSuccess:mutationData => {- queryClient.setQueryData(['livros'], data =>- data.map(book =>- book.id ===mutationData.id ? { ...book, . ..mutationData } : book,- ),- );- queryClient.setQueryData(['book'], data =>- data.id ===mutationData.id ? { ...data, ...mutationData } : dados,- );- },}); const updateBookAuthorMutation = useMutation({ mutaçãoFn: () => ({ identificação: '1', autor: {id: '1004', nome: 'User4' }, }),- onSuccess:mutationData => {- queryClient.setQueryData(['livros'], data =>- data.map(book =>- book.id ===mutationData.id ? { ...book, . ..mutationData } : book,- ),- );- queryClient.setQueryData(['book'], data =>- data.id ===mutationData.id ? { ...data, ...mutationData } : dados,- );- },}); const addBookMutation = useMutation({ mutaçãoFn: () => ({ identificação: '3', nome: 'Nome 3', autor: {id: '1003', nome: 'User3' }, }), // com dados com arrays de nível superior, você ainda precisa atualizar os dados manualmente onSuccess:mutationData => { queryClient.setQueryData(['livros'], dados => data.concat(mutationData)); }, }); // retorna algum JSX }; const App = () => (+ <QueryNormalizerProvider queryClient={queryClient}> <QueryClientProvider client={queryClient}> <Livros /> </QueryClientProvider>+ </QueryNormalizerProvider> );
Portanto, como você pode ver, além dos arrays de nível superior, nenhuma atualização manual de dados é mais necessária. Isto é especialmente útil se uma determinada mutação atualizar dados para múltiplas consultas. Não apenas isso é detalhado para fazer atualizações manualmente, mas também você precisa saber exatamente quais consultas atualizar. Quanto mais dúvidas você tiver, maiores vantagens normy
traz.
Como funciona? Por padrão, todos os objetos com chave id
são organizados por seus ids. Agora, qualquer objeto com chave id
será normalizado, o que significa simplesmente armazenado por id. Se já existir um objeto correspondente com o mesmo id, um novo será profundamente mesclado com aquele que já está no estado. Portanto, se os dados de resposta do servidor de uma mutação forem { id: '1', title: 'new title' }
, esta biblioteca descobrirá automaticamente como atualizar title
do objeto com id: '1'
para todas as consultas dependentes.
Também funciona com objetos aninhados com ids, não importa a profundidade. Se um objeto com id tiver outros objetos com ids, então eles serão normalizados separadamente e o objeto pai terá apenas referência a esses objetos aninhados.
Para instalar o pacote, basta executar:
$ npm install @normy/react-query
ou você pode simplesmente usar o CDN: https://unpkg.com/@normy/react-query
.
Para instalar o pacote, basta executar:
$ npm install @normy/swr
ou você pode simplesmente usar o CDN: https://unpkg.com/@normy/swr
.
Para instalar o pacote, basta executar:
$ npm install @normy/rtk-query
ou você pode simplesmente usar o CDN: https://unpkg.com/@normy/rtk-query
.
Se você quiser escrever um plugin para outra biblioteca que não seja react-query
, swr
ou rtk-query
:
$ npm install @normy/core
ou você pode simplesmente usar o CDN: https://unpkg.com/@normy/core
.
Para ver como escrever um plugin, por enquanto basta verificar o código fonte do @normy/react-query
, é muito fácil de fazer, futuramente será criado um guia.
Para que a normalização automática funcione, as seguintes condições devem ser atendidas:
você deve ter uma forma padronizada de identificar seus objetos, geralmente isso é feito por key id
os ids devem ser exclusivos em todo o aplicativo, não apenas nos tipos de objetos; caso contrário, você precisará anexar algo a eles, o mesmo deve ser feito no mundo GraphQL, geralmente adicionando _typename
objetos com os mesmos ids devem ter uma estrutura consistente, se um objeto como livro em uma consulta tiver chave title
, deve ser title
em outras, e não name
repentino
Existe uma função que pode ser passada para createQueryNormalizer
para atender a esses requisitos, nomeadamente getNormalizationObjectKey
.
getNormalizationObjectKey
pode ajudá-lo no primeiro ponto, se por exemplo você identificar objetos de forma diferente, como pela chave _id
, então você pode passar getNormalizationObjectKey: obj => obj._id
.
getNormalizationObjectKey
também permite que você passe no segundo requisito. Por exemplo, se seus IDs forem únicos, mas não em todo o aplicativo, mas dentro dos tipos de objetos, você poderá usar getNormalizationObjectKey: obj => obj.id && obj.type ? obj.id + obj.type : undefined
ou algo semelhante. Se isso não for possível, você mesmo pode calcular um sufixo, por exemplo:
const getType = obj => { if (obj.bookTitle) {return 'livro'; } if (obj.sobrenome) {retornar 'usuário'; } return indefinido;};createQueryNormalizer(queryClient, { getNormalizationObjectKey: obj =>obj.id && getType(obj) && obj.id + getType(obj),});
O ponto 3 deve sempre ser atendido; caso contrário, você realmente deve pedir aos desenvolvedores de back-end para manter as coisas padronizadas e consistentes. Como último recurso, você pode alterar as respostas de sua parte.
Infelizmente, isso não significa que você nunca mais precisará atualizar os dados manualmente. Algumas atualizações ainda precisam ser feitas manualmente, como normalmente, ou seja, adicionar e remover itens do array. Por que? Imagine uma mutação REMOVE_BOOK
. Este livro pode estar presente em muitas consultas, a biblioteca não pode saber de quais consultas você gostaria de removê-lo. O mesmo se aplica a ADD_BOOK
, a biblioteca não pode saber a qual consulta um livro deve ser adicionado, ou mesmo qual índice de array. A mesma coisa para ações como SORT_BOOKS
. No entanto, esse problema afeta apenas matrizes de nível superior. Por exemplo, se você tiver um livro com algum id e outra chave como likedByUsers
, se você retornar um novo livro com a lista atualizada em likedByUsers
, isso funcionará novamente automaticamente.
Porém, na versão futura da biblioteca, com algumas dicas adicionais, também será possível fazer as atualizações acima!
Se você estiver interessado em quais manipulações de dados normy
realmente faz, você pode usar a opção devLogging
:
<QueryNormalizerProvider queryClient={queryClient} normalizerConfig={{ devLogging: true }}> {crianças}</QueryNormalizerProvider>
false
por padrão, se definido como true
, você poderá ver nas informações do console quando as consultas são definidas ou removidas.
Observe que isso funciona apenas no desenvolvimento, mesmo se você passar true
, nenhum registro será feito na produção (quando precisamente process.env.NODE_ENV === 'production'
). NODE_ENV
geralmente é definido por empacotadores de módulos como webpack
para você, então provavelmente você não precisa se preocupar em configurar NODE_ENV
você mesmo.
Como sempre, qualquer automatização tem um custo. No futuro, alguns benchmarks poderão ser adicionados, mas por enquanto os testes manuais mostraram que, a menos que em seus dados você tenha dezenas de milhares de objetos normalizados, a sobrecarga não deverá ser perceptível. No entanto, você tem várias maneiras flexíveis de melhorar o desempenho:
Você pode normalizar apenas consultas que possuem atualizações de dados e apenas mutações que devem atualizar os dados - é isso, você pode normalizar apenas parte dos seus dados. Confira na documentação de integração como fazer isso.
Como 1.
, mas para consultas e mutações com dados extremamente grandes.
Há uma otimização integrada, que verifica os dados das respostas de mutação se eles são realmente diferentes dos dados no armazenamento normalizado. Se for igual, as consultas dependentes não serão atualizadas. Portanto, é bom que os dados de mutação incluam apenas coisas que poderiam realmente ser diferentes, o que poderia evitar normalizações desnecessárias e atualizações de consultas.
Não desabilite a opção structuralSharing
em bibliotecas que a suportam - se os dados de uma consulta após a atualização forem iguais referencialmente como antes da atualização, então esta consulta não será normalizada. Esta é uma grande otimização de desempenho, especialmente após a nova busca no refocus, que pode atualizar várias consultas ao mesmo tempo, geralmente para os mesmos dados.
Você pode usar a função getNormalizationObjectKey
para definir globalmente quais objetos devem ser realmente normalizados. Por exemplo:
<QueryNormalizerProvider queryClient={queryClient} normalizerConfig={{getNormalizationObjectKey: obj => (obj.normalizable? obj.id: indefinido), }}> {crianças}</QueryNormalizerProvider>
Além disso, no futuro serão adicionadas algumas opções adicionais específicas de desempenho.
Atualmente existem três integrações oficiais com bibliotecas de busca de dados, nomeadamente react-query
, swr
e rtk-query
. Veja documentações dedicadas para integrações específicas:
consulta de reação
swr
consulta rtk
Eu recomendo fortemente experimentar exemplos de como este pacote pode ser usado em aplicações reais.
Existem os seguintes exemplos atualmente:
consulta de reação
trpc
swr
consulta rtk
MIT