normy

repository·master·Indexed 20 days ago

https://github.com/klis87/normy

A data normalization library providing automatic synchronization across queries for @tanstack/query and Redux Toolkit Query. It includes framework-specific integrations such as @normy/react-query (supporting react-query 5 and trpc 11), @normy/vue-query, and @normy/rtk-query, as well as a low-level utility library @normy/query for creating addons.

Tokens
23K
Snippets
77
Records
94
Agent score
69%

What's inside normy

  1. Available normy integrations and examples

    master

    normy provides official integrations for several data-fetching libraries. Use the dedicated documentation and examples to learn how to implement them in your specific stack.

    Official Integrations

    • react-query
    • vue-query
    • swr
    • rtk-query

    Real-world Examples

    To see how normy is used in actual applications, refer to these example repositories:

    • react-query
    • trpc
    • vue-query
    • swr
    • rtk-query
  2. Understand the purpose of @normy/query

    master

    The @normy/query package is a low-level utility library designed to simplify the creation of addons for @tanstack/query libraries.

    Important: This library is not intended for direct use by end-users. Instead, you should use the high-level libraries provided for specific frameworks, such as @normy/react-query or @normy/vue-query.

  3. What is Normy and how does it work?

    master

    Normy is a library for automatic data normalization and automatic updates for data fetching libraries like react-query, swr, and RTK Query.

    Core Mechanism

    • Normalization: By default, any object containing an id key is normalized (stored by its ID). If an object with the same ID is encountered again, it is deeply merged with the existing state.
    • Automatic Updates: When a mutation returns data, Normy identifies the objects by their IDs and updates them across all dependent queries in your application state. This eliminates the need for manual onSuccess cache updates.
    • Nested Objects: It handles nested objects with IDs by normalizing them separately and replacing the parent's property with a reference to the normalized object.
    • Array Operations: Normy supports array operations (like appending) using special hints in the mutation data, such as __append: 'arrayKey'.
    // Example of how a mutation data object can trigger an array append
    // instead of manual cache manipulation
    {
      id: '3',
      name: 'Name 3',
      author: { id: '1003', name: 'User3' },
      __append: 'books'
    }
  4. How @normy/vue-query works with TanStack Vue Query

    master

    The @normy/vue-query package provides automatic data normalization and updates for your application. When you use VueQueryNormalizerPlugin, normy inspects the response data from your queries and mutations. It automatically updates all dependent queries that contain the same normalized entities. This eliminates the need for manual queryClient.setQueryData calls in onSuccess handlers for most scenarios, especially when a single mutation affects multiple queries.

    import { createApp } from 'vue';
    import {
      VueQueryPlugin,
      QueryClient,
      useQueryClient,
    } from '@tanstack/react-query';
    + import { VueQueryNormalizerPlugin } from '@normy/vue-query';
    
    <script>
      const queryClient = useQueryClient();
    
      // ... useQuery calls ...
    
      const updateBookNameMutation = useMutation({
        mutationFn: () => ({ id: '1', name: 'Name 1 Updated' }),
    -     onSuccess: mutationData => {
    -       queryClient.setQueryData(['books'], data => ...);
    -       queryClient.setQueryData(['book'], data => ...);
    -     },
      });
    </script>
    
    const queryClient = new QueryClient();
    
    createApp(App)
    +   .use(VueQueryNormalizerPlugin, { queryClient })
        .use(VueQueryPlugin, { queryClient })
        .mount('#app');
  5. Handle recursive relationships in getObjectById

    master

    When using getObjectById on objects with recursive relationships (e.g., a user whose bestFriend is also a user), you may encounter infinite recursion errors or receive undefined.

    To prevent this and control the returned structure, pass a second argument to getObjectById representing the desired data structure. The actual values in this structure do not matter (e.g., use empty strings); only the data types are important.

    Benefits of providing a structure:

    • Prevents infinite recursion: Limits the depth of denormalization.
    • Controls structure: Returns only the specific fields you need.
    • TypeScript support: Automatically provides proper typing for the returned object based on the structure provided.
    // Example: Preventing infinite recursion in a user/friend relationship
    const user = normalizer.getObjectById('1', {
      id: '',
      name: '',
      bestFriend: { id: '', name: '' },
    });
    
    // Resulting structure:
    // {
    //   id: '1',
    //   name: 'X',
    //   bestFriend: { id: '2', name: 'Y' }
    // }
  6. Performance: Structural sharing and Garbage collection

    master

    Structural Sharing

    normy leverages vue-query's structural sharing. If a query is refetched but the API response is structurally identical to the existing data, normy will skip the normalization process. This provides significant performance improvements during mass refetches (e.g., on window refocus).

    Note: Do not disable vue-query structural sharing, as normy relies on it for optimization.

    Garbage Collection

    normy automatically performs garbage collection. When a query is removed from the vue-query cache, normy removes all redundant normalized information associated with that query.

  7. How @normy/swr works with SWR

    master

    @normy/swr is an integration that enables automatic data normalization and updates for SWR.

    Instead of manually calling mutate for every query that might be affected by a mutation (which is verbose and error-prone), @normy/swr inspects mutation response data, calculates which normalized entities have changed, and automatically updates all relevant SWR queries.

    Key workflow changes:

    1. Wrap your application in SWRNormalizerProvider.
    2. Use useNormalizedSWRMutation instead of useSWRMutation for mutations that should trigger automatic updates across the app.
    3. For top-level arrays (e.g., a list of items), you still need to update the array manually using mutate in the onSuccess callback, as normalization primarily handles individual object updates within queries.
      import useSWR, { useSWRConfig } from 'swr';
    + import { SWRNormalizerProvider, useNormalizedSWRMutation } from '@normy/swr';
    
      const Books = () => {
        // Standard SWR usage works as normal
        const { data: booksData = [] } = useSWR('/books', fetchBooks);
    
    -   const updateBookNameMutation = useSWRMutation('/book/update-name', updateFn, {
    -     onSuccess: mutationData => {
    -       mutate('/books', data => data.map(book => ...));
    -       mutate('/book', data => ...);
    -     }
    -   });
    +   // Automatic updates: no manual mutate calls needed for object properties
    +   const updateBookNameMutation = useNormalizedSWRMutation('/book/update-name', updateFn);
      };
    
      const App = () => (
    -   <SWRConfig value={someSwrConfig}>
    +   <SWRNormalizerProvider swrConfigValue={someSwrConfig}>
          <Books />
    -   </SWRConfig>
    +   </SWRNormalizerProvider>
      );
  8. Understand lifecycle and optimization behaviors

    master

    Garbage Collection

    normy automatically cleans up the store. When a query is removed from the store, redundant information associated with that query is also removed.

    Clearing Data

    All normalized data is automatically cleared when the SWRNormalizerProvider is unmounted.

    Structural Sharing

    normy leverages SWR's structural sharing. If a query is refetched but the API response is structurally identical to the existing data, normy avoids unnecessary re-normalization, which helps optimize performance and prevent unnecessary re-renders.

  9. Use array operation hints for automatic updates

    master

    To automatically update arrays during a mutation, decorate your mutation response objects with special meta properties (hints). These hints tell Normy which array types to modify and how.

    Node Operations

    These are applied to a specific object (node) to modify an array it belongs to:

    • __append: Adds the node to an array. Can be a string or an array of strings: __append: 'books' or __append: ['books', 'favorites'].
    • __insert: Inserts the node at a specific index.
      • Format: { __insert: { arrayTypes: 'books', index: 1 } } or { __insert: { arrayTypes: ['books', 'favs'], index: 1 } }.
      • Supports negative indexes (e.g., -1 for the end).
    • __prepend: Inserts the node at index 0.
    • __remove: Removes the node from an array by its ID.
    • __replace: Replaces a node at a specific index.
    • __move: Moves the node to a new index.
    • __swap: Swaps the node with the node at the target index.

    Nodeless Operations

    These act on an array type directly without being attached to a specific node:

    • __clear: Clears an entire array. Example: { __clear: 'books' }.
    • __replaceAll: Replaces an array with a completely new value. Example: { __replaceAll: { arrayType: 'books', value: [...] } }.
  10. Optimize performance in normy

    master

    While normy is efficient for most datasets, you can use several strategies to improve performance if you are dealing with tens of thousands of objects or extremely large data:

    1. Partial Normalization: Only normalize specific queries that have data updates or mutations that should trigger data updates. Refer to the specific integration documentation for implementation details.
    2. Selective Mutation Data: Ensure mutation responses include only data that could actually change. normy performs a built-in optimization that checks if mutation data differs from the existing normalized store; if they are identical, dependent queries will not be updated.
    3. Enable Structural Sharing: Do not disable the structuralSharing option in your underlying data-fetching library. If query data remains referentially identical after an update, normy will skip normalization for that query.
    4. Filter Normalizable Objects: Use the getNormalizationObjectKey configuration option to globally define which objects should be normalized. This allows you to specify a key (like an id) only for objects that meet certain criteria (e.g., having a normalizable property).
    <QueryNormalizerProvider
      queryClient={queryClient}
      normalizerConfig={{
        getNormalizationObjectKey: obj => (obj.normalizable ? obj.id : undefined),
      }}
    >
      {children}
    </QueryNormalizerProvider>
  11. Lifecycle and Performance: Garbage Collection and Structural Sharing

    master

    Garbage Collection

    normy automatically cleans up the normalized store. When a query is removed from the store, normy removes all redundant information associated with it.

    Unsubscribing

    When the QueryNormalizerProvider is unmounted, all normalized data is automatically cleared and all subscribers to the react-query client are unsubscribed.

    Structural Sharing

    normy leverages react-query's structural sharing. If a query is refetched but the API response is structurally identical to the existing data, normy avoids re-normalizing the data. This provides significant performance improvements during frequent refetches (e.g., on window refocus) by preventing unnecessary normalization work.