Skip to content

Commit ac24aa1

Browse files
committed
Stabilize DataState useSubset() implementation
1 parent ace5600 commit ac24aa1

File tree

3 files changed

+26
-12
lines changed

3 files changed

+26
-12
lines changed

src/components/content/home-page/transactions/transaction.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ export default function Transaction(props: {
1313
blockWithTransactionsData: DataState<BlockWithTransactions>,
1414
}) {
1515
const txData = props.blockWithTransactionsData.useSubset(
16-
data => ({ ...data.transactions[props.id] })
16+
(data, id) => ({ ...data.transactions[id] }),
17+
[props.id]
1718
);
1819

1920
return (

src/lib/data-state/constructors/index.tsx

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
1+
import { DependencyList, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
22
import LoadingPulse from '@/components/common/indicators/loading-pulse';
33
import LoadingIndicator from '@/components/common/indicators/loading-indicator';
44
import ErrorIndicator from '@/components/common/indicators/error-indicator';
@@ -37,8 +37,9 @@ export const useConfig: DataStateConstructor = <T, A extends any[], R, P>(config
3737
const setError = (unknownError: unknown, prefix?: string) => setRoot(DataRoot.error(unknownError, prefix));
3838

3939
// Stabilize args and default to empty array if no args provided:
40-
41-
const args = useMemo(() => config.args || [] as unknown as A, config.args || [] as unknown as A);
40+
const args = useMemo(() => config.args || [] as unknown as A,
41+
// eslint-disable-next-line react-hooks/exhaustive-deps
42+
config.args || [] as unknown as A);
4243

4344
// Stabilize fetcher: only create it once and don't update it.
4445
// Even if the parent re-creates the fetcher each render (e.g. when defined
@@ -123,11 +124,20 @@ export const useConfig: DataStateConstructor = <T, A extends any[], R, P>(config
123124
}
124125

125126
// Create a new DataState containing a subset of the fields of another:
126-
const useSubset = <S,>(selector: (data: T) => S): DataState<S> => {
127-
const subsetPostProcess = (response: R): S => {
127+
const useSubset = <S, B extends any[]>(
128+
selectorFn: (data: T, ...args: B) => S,
129+
selectorArgs: B,
130+
): DataState<S> => {
131+
// Stabilize the selector function once - it never changes:
132+
const stableSelector = useRef(selectorFn).current;
133+
// Stabilize the arguments:
134+
// eslint-disable-next-line react-hooks/exhaustive-deps
135+
const stableArgs = useMemo(() => selectorArgs, selectorArgs);
136+
137+
const subsetPostProcess = useCallback((response: R): S => {
128138
const result = (postProcess ? postProcess(response) : response) as unknown as T;
129-
return selector(result);
130-
};
139+
return stableSelector(result, ...stableArgs);
140+
}, [stableArgs, postProcess, stableSelector]);
131141

132142
const subsetConfig = {
133143
fetcher: fetcher,
@@ -144,7 +154,7 @@ export const useConfig: DataStateConstructor = <T, A extends any[], R, P>(config
144154
break;
145155
case 'value':
146156
try {
147-
const selectedValue = selector(dataRoot.value);
157+
const selectedValue = stableSelector(dataRoot.value, ...stableArgs);
148158
subsetData.setRoot(DataRoot.value(selectedValue));
149159
} catch (err) {
150160
// Handle selector errors gracefully:
@@ -159,7 +169,8 @@ export const useConfig: DataStateConstructor = <T, A extends any[], R, P>(config
159169
subsetData.setRoot(DataRoot.error(dataRoot.error));
160170
break;
161171
}
162-
}, [dataRoot]);
172+
// eslint-disable-next-line react-hooks/exhaustive-deps
173+
}, [dataRoot, stableArgs]);
163174

164175
return subsetData;
165176
}

src/lib/data-state/types/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Dispatch, ReactNode, SetStateAction } from 'react';
1+
import { DependencyList, Dispatch, ReactNode, SetStateAction } from 'react';
22

33

44
export type LoadingRoot = {
@@ -53,7 +53,9 @@ export type DataStateMethods<T> = {
5353
// and render that, or otherwise default to rendering the DataState's value directly.
5454
Render: <K extends keyof T>(options?: RenderConfig<T, K>) => ReactNode;
5555
// Create a new DataState containing a subset of the fields of another:
56-
useSubset: <S>(selector: (data: T) => S) => DataState<S>;
56+
useSubset: <S, A extends any[]>(
57+
selectorFn: (data: T, ...args: A) => S,
58+
args: A) => DataState<S>;
5759
// compose: <X, Y>(dataState: DataState<X>) => DataState<Y>;
5860
};
5961

0 commit comments

Comments
 (0)