diff --git a/examples/react-query/src/index.jsx b/examples/react-query/src/index.jsx index f90b600..2b0d9d1 100644 --- a/examples/react-query/src/index.jsx +++ b/examples/react-query/src/index.jsx @@ -6,13 +6,7 @@ import { QueryNormalizerProvider } from '@normy/react-query'; import App from './components/app'; -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - refetchOnWindowFocus: false, - }, - }, -}); +const queryClient = new QueryClient(); const renderApp = () => { const container = document.getElementById('root'); diff --git a/packages/normy-react-query/README.md b/packages/normy-react-query/README.md index f3b18b4..b52d6cd 100644 --- a/packages/normy-react-query/README.md +++ b/packages/normy-react-query/README.md @@ -28,6 +28,7 @@ - [getObjectById and getQueryFragment](#getObjectById-and-getQueryFragment-arrow_up) - [Garbage collection](#garbage-collection-arrow_up) - [Clearing and unsubscribing from updates](#clearing-and-unsubscribing-from-updates-arrow_up) +- [Structural sharing](#structural-sharing-arrow_up) - [Examples](#examples-arrow_up) ## Introduction [:arrow_up:](#table-of-content) @@ -454,6 +455,21 @@ information. When `QueryNormalizerProvider` is unmounted, all normalized data will be automatically cleared and all subscribers to `react-query` client will be unsubscribed. +## Structural sharing [:arrow_up:](#table-of-content) + +By default, this library takes advantage over `react-query` structural sharing feature. Structural sharing benefit is the following - if a query +is refetched, its data will remain referentially the same if it is the same structurally (when API response is the same). + +Typically it was implemented in order to have optimizations like avoiding rerenders for the same data, +but `normy` also takes advantage over it, namely, if a query was just refetched but its data is the same, +`normy` will not unnecessarily normalize it (as it would normalize it to the same value it has now anyway). + +This brings big performance improvements, especially during refetches on window refocus (if you use this feature), as then +potentially dozens of queries could be refetched simultaneously. In practice, most of those responses will be the same, +which will prevent data to be normalized again unnecessarily (to the very same normalized value). + +So it is even more beneficial not to turn off `react-query` structural sharing feature! + ## Examples [:arrow_up:](#table-of-content) I highly recommend to try examples how this package could be used in real applications. diff --git a/packages/normy-react-query/src/QueryNormalizerProvider.tsx b/packages/normy-react-query/src/QueryNormalizerProvider.tsx index cec2b77..0713560 100644 --- a/packages/normy-react-query/src/QueryNormalizerProvider.tsx +++ b/packages/normy-react-query/src/QueryNormalizerProvider.tsx @@ -15,7 +15,9 @@ export const QueryNormalizerProvider = ({ }: { queryClient: QueryClient; children: React.ReactNode; - normalizerConfig?: NormalizerConfig & { normalize?: boolean }; + normalizerConfig?: Omit & { + normalize?: boolean; + }; }) => { const [queryNormalizer] = React.useState(() => createQueryNormalizer(queryClient, normalizerConfig), diff --git a/packages/normy-react-query/src/create-query-normalizer.ts b/packages/normy-react-query/src/create-query-normalizer.ts index 8bf62fe..182859b 100644 --- a/packages/normy-react-query/src/create-query-normalizer.ts +++ b/packages/normy-react-query/src/create-query-normalizer.ts @@ -33,7 +33,9 @@ const updateQueriesFromMutationData = ( export const createQueryNormalizer = ( queryClient: QueryClient, - normalizerConfig: NormalizerConfig & { normalize?: boolean } = {}, + normalizerConfig: Omit & { + normalize?: boolean; + } = {}, ) => { const normalize = normalizerConfig.normalize ?? true; const normalizer = createNormalizer(normalizerConfig); diff --git a/packages/normy-rtk-query/src/index.ts b/packages/normy-rtk-query/src/index.ts index 6ac2073..b58aa54 100644 --- a/packages/normy-rtk-query/src/index.ts +++ b/packages/normy-rtk-query/src/index.ts @@ -78,12 +78,16 @@ const isNormalizerAction = (action: unknown): action is NormalizerAction => export const createNormalizationMiddleware = ( api: ReturnType, - normalizerConfig?: NormalizerConfig & { + normalizerConfig?: Omit & { normalizeQuery?: (queryType: string) => boolean; normalizeMutation?: (mutationEndpointName: string) => boolean; }, ): Middleware => { - const normalizer = createNormalizer(normalizerConfig); + const normalizer = createNormalizer({ + ...normalizerConfig, + // TODO: we wait for rtk-query maintainers to make this work + structuralSharing: false, + }); const args: Record = {}; diff --git a/packages/normy-swr/README.md b/packages/normy-swr/README.md index d2db457..505b4c8 100644 --- a/packages/normy-swr/README.md +++ b/packages/normy-swr/README.md @@ -23,6 +23,7 @@ - [getObjectById and getQueryFragment](#getObjectById-and-getQueryFragment-arrow_up) - [Garbage collection](#garbage-collection-arrow_up) - [Clearing](#clearing-arrow_up) +- [Structural sharing](#structural-sharing-arrow_up) - [Examples](#examples-arrow_up) ## Introduction [:arrow_up:](#table-of-content) @@ -402,6 +403,15 @@ information. When `SWRNormalizerProvider` is unmounted, all normalized data will be automatically cleared. +## Structural sharing [:arrow_up:](#table-of-content) + +By default, this library takes advantage over `swr` structural sharing feature. Structural sharing benefit is the following - if a query +is refetched, its data will remain referentially the same if it is the same structurally (when API response is the same). + +Typically it was implemented in order to have optimizations like avoiding rerenders for the same data, +but `normy` also takes advantage over it, namely, if a query was just refetched but its data is the same, +`normy` will not unnecessarily normalize it (as it would normalize it to the same value it has now anyway). + ## Examples [:arrow_up:](#table-of-content) I highly recommend to try examples how this package could be used in real applications. diff --git a/packages/normy-swr/src/SWRNormalizerProvider.tsx b/packages/normy-swr/src/SWRNormalizerProvider.tsx index 4232ccd..b1fb19c 100644 --- a/packages/normy-swr/src/SWRNormalizerProvider.tsx +++ b/packages/normy-swr/src/SWRNormalizerProvider.tsx @@ -7,7 +7,7 @@ import { import { useSWRConfig, SWRConfig, type Key } from 'swr'; const createSwrNormalizer = ( - normalizerConfig: NormalizerConfig & { + normalizerConfig: Omit & { normalize?: (queryKey: string) => boolean; } = {}, ) => { @@ -81,7 +81,7 @@ export const SWRNormalizerProvider = ({ swrConfigValue, children, }: { - normalizerConfig?: NormalizerConfig & { + normalizerConfig?: Omit & { normalize: (queryKey: Key) => boolean; }; swrConfigValue: React.ComponentProps['value']; diff --git a/packages/normy/README.md b/packages/normy/README.md index 3a57563..e08f960 100644 --- a/packages/normy/README.md +++ b/packages/normy/README.md @@ -267,7 +267,8 @@ However, you have several flexible ways to improve performance: 3. There is a built-in optimalization, which checks data from mutation responses if they are actually different than data in the normalized store. If it is the same, dependent queries will not be updated. So, it is good for mutation data to include only things which could actually be different, which could prevent unnecessary normalization and queries updates. -4. You can use `getNormalizationObjectKey` function to set globally which objects should be actually normalized. For example: +4. Do not disable `structuralSharing` option in libraries which support it - if a query data after update is the same referentially as before update, then this query will not be normalized. This is a big performance optimization, especially after refetch on refocus, which could update multiple queries at the same time, usually to the very same data. +5. You can use `getNormalizationObjectKey` function to set globally which objects should be actually normalized. For example: ```jsx { dependentQueries: { '@@1': ['query'] }, }); }); + + it('does not update normalized data when using structural sharing and data is the same', () => { + const normalizer = createNormalizer(); + const data = { id: '1', name: 'name' }; + normalizer.setQuery('query', data); + const normalizedData = normalizer.getNormalizedData(); + normalizer.setQuery('query', data); + + expect(normalizedData).toBe(normalizer.getNormalizedData()); + }); + + it('updates normalized data when using structural sharing but data is not the same', () => { + const normalizer = createNormalizer(); + normalizer.setQuery('query', { id: '1', name: 'name' }); + const normalizedData = normalizer.getNormalizedData(); + normalizer.setQuery('query', { id: '1', name: 'name' }); + + expect(normalizedData).not.toBe(normalizer.getNormalizedData()); + }); + + it('updates normalized data when data is the same but without using structural sharing', () => { + const normalizer = createNormalizer({ structuralSharing: false }); + const data = { id: '1', name: 'name' }; + normalizer.setQuery('query', data); + const normalizedData = normalizer.getNormalizedData(); + normalizer.setQuery('query', data); + + expect(normalizedData).not.toBe(normalizer.getNormalizedData()); + }); }); describe('removeQuery', () => { diff --git a/packages/normy/src/create-normalizer.ts b/packages/normy/src/create-normalizer.ts index f2f0a21..642a0bc 100644 --- a/packages/normy/src/create-normalizer.ts +++ b/packages/normy/src/create-normalizer.ts @@ -58,8 +58,17 @@ export const createNormalizer = ( const config = { ...defaultConfig, ...normalizerConfig }; let normalizedData: NormalizedData = initialNormalizedData ?? initialData; + let currentDataReferences: Record = {}; const setQuery = (queryKey: string, queryData: Data) => { + if (config.structuralSharing) { + if (currentDataReferences[queryKey] === queryData) { + return; + } + + currentDataReferences[queryKey] = queryData; + } + const [normalizedQueryData, normalizedObjectsData, usedKeys] = normalize( queryData, config, @@ -106,6 +115,7 @@ export const createNormalizer = ( const queries = { ...normalizedData.queries }; delete queries[queryKey]; + delete currentDataReferences[queryKey]; normalizedData = { ...normalizedData, @@ -206,6 +216,7 @@ export const createNormalizer = ( getNormalizedData: () => normalizedData, clearNormalizedData: () => { normalizedData = initialData; + currentDataReferences = {}; }, setQuery, removeQuery, diff --git a/packages/normy/src/default-config.ts b/packages/normy/src/default-config.ts index 80182e8..1a6b064 100644 --- a/packages/normy/src/default-config.ts +++ b/packages/normy/src/default-config.ts @@ -3,4 +3,5 @@ import { NormalizerConfig } from './types'; export const defaultConfig: Required = { getNormalizationObjectKey: obj => obj.id as string | undefined, devLogging: false, + structuralSharing: true, }; diff --git a/packages/normy/src/normalize.spec.ts b/packages/normy/src/normalize.spec.ts index 23affe3..3efaae0 100644 --- a/packages/normy/src/normalize.spec.ts +++ b/packages/normy/src/normalize.spec.ts @@ -318,6 +318,7 @@ describe('normalize', () => { ? `${obj._id as string}${obj.key as string}` : undefined, devLogging: false, + structuralSharing: true, }, ), ).toEqual([ diff --git a/packages/normy/src/types.ts b/packages/normy/src/types.ts index 1b8fbca..f38763e 100644 --- a/packages/normy/src/types.ts +++ b/packages/normy/src/types.ts @@ -22,6 +22,7 @@ export type Data = export type NormalizerConfig = { getNormalizationObjectKey?: (obj: DataObject) => string | undefined; devLogging?: boolean; + structuralSharing?: boolean; }; export type UsedKeys = { [path: string]: ReadonlyArray };