Skip to content

Commit

Permalink
Add structural sharing
Browse files Browse the repository at this point in the history
  • Loading branch information
klis87 committed Feb 13, 2024
1 parent effb907 commit b1b2f6b
Show file tree
Hide file tree
Showing 13 changed files with 86 additions and 14 deletions.
8 changes: 1 addition & 7 deletions examples/react-query/src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
16 changes: 16 additions & 0 deletions packages/normy-react-query/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion packages/normy-react-query/src/QueryNormalizerProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ export const QueryNormalizerProvider = ({
}: {
queryClient: QueryClient;
children: React.ReactNode;
normalizerConfig?: NormalizerConfig & { normalize?: boolean };
normalizerConfig?: Omit<NormalizerConfig, 'structuralSharing'> & {
normalize?: boolean;
};
}) => {
const [queryNormalizer] = React.useState(() =>
createQueryNormalizer(queryClient, normalizerConfig),
Expand Down
4 changes: 3 additions & 1 deletion packages/normy-react-query/src/create-query-normalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ const updateQueriesFromMutationData = (

export const createQueryNormalizer = (
queryClient: QueryClient,
normalizerConfig: NormalizerConfig & { normalize?: boolean } = {},
normalizerConfig: Omit<NormalizerConfig, 'structuralSharing'> & {
normalize?: boolean;
} = {},
) => {
const normalize = normalizerConfig.normalize ?? true;
const normalizer = createNormalizer(normalizerConfig);
Expand Down
8 changes: 6 additions & 2 deletions packages/normy-rtk-query/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,16 @@ const isNormalizerAction = (action: unknown): action is NormalizerAction =>

export const createNormalizationMiddleware = (
api: ReturnType<typeof createApi>,
normalizerConfig?: NormalizerConfig & {
normalizerConfig?: Omit<NormalizerConfig, 'structuralSharing'> & {
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<string, unknown> = {};

Expand Down
10 changes: 10 additions & 0 deletions packages/normy-swr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions packages/normy-swr/src/SWRNormalizerProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import { useSWRConfig, SWRConfig, type Key } from 'swr';

const createSwrNormalizer = (
normalizerConfig: NormalizerConfig & {
normalizerConfig: Omit<NormalizerConfig, 'structuralSharing'> & {
normalize?: (queryKey: string) => boolean;
} = {},
) => {
Expand Down Expand Up @@ -81,7 +81,7 @@ export const SWRNormalizerProvider = ({
swrConfigValue,
children,
}: {
normalizerConfig?: NormalizerConfig & {
normalizerConfig?: Omit<NormalizerConfig, 'structuralSharing'> & {
normalize: (queryKey: Key) => boolean;
};
swrConfigValue: React.ComponentProps<typeof SWRConfig>['value'];
Expand Down
3 changes: 2 additions & 1 deletion packages/normy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<QueryNormalizerProvider
Expand Down
29 changes: 29 additions & 0 deletions packages/normy/src/create-normalizer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,35 @@ describe('createNormalizer', () => {
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', () => {
Expand Down
11 changes: 11 additions & 0 deletions packages/normy/src/create-normalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,17 @@ export const createNormalizer = (
const config = { ...defaultConfig, ...normalizerConfig };

let normalizedData: NormalizedData = initialNormalizedData ?? initialData;
let currentDataReferences: Record<string, Data> = {};

const setQuery = (queryKey: string, queryData: Data) => {
if (config.structuralSharing) {
if (currentDataReferences[queryKey] === queryData) {
return;
}

currentDataReferences[queryKey] = queryData;
}

const [normalizedQueryData, normalizedObjectsData, usedKeys] = normalize(
queryData,
config,
Expand Down Expand Up @@ -106,6 +115,7 @@ export const createNormalizer = (

const queries = { ...normalizedData.queries };
delete queries[queryKey];
delete currentDataReferences[queryKey];

normalizedData = {
...normalizedData,
Expand Down Expand Up @@ -206,6 +216,7 @@ export const createNormalizer = (
getNormalizedData: () => normalizedData,
clearNormalizedData: () => {
normalizedData = initialData;
currentDataReferences = {};
},
setQuery,
removeQuery,
Expand Down
1 change: 1 addition & 0 deletions packages/normy/src/default-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { NormalizerConfig } from './types';
export const defaultConfig: Required<NormalizerConfig> = {
getNormalizationObjectKey: obj => obj.id as string | undefined,
devLogging: false,
structuralSharing: true,
};
1 change: 1 addition & 0 deletions packages/normy/src/normalize.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ describe('normalize', () => {
? `${obj._id as string}${obj.key as string}`
: undefined,
devLogging: false,
structuralSharing: true,
},
),
).toEqual([
Expand Down
1 change: 1 addition & 0 deletions packages/normy/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> };
Expand Down

0 comments on commit b1b2f6b

Please sign in to comment.