Skip to content

Commit 29e5c64

Browse files
committed
fix(FR-1401): fix state persistence issue in useDeferredQueryParams hook (#4179)
Resolves #4178 ([FR-1401](https://lablup.atlassian.net/browse/FR-1401)) ## Problem The `useDeferredQueryParams` hook was experiencing a state persistence issue where query parameters set on one page would unexpectedly remain when navigating to another page. This occurred because the global `queryParamsAtom` was shared across all pages without proper cleanup mechanisms. ## Root Cause - The hook used a global atom (`queryParamsAtom`) to store query parameters - No cleanup logic existed when components unmounted - Parameters from previous pages would persist in the global state and affect newly mounted components ## Solution Implemented Implemented a reference counting mechanism to properly manage shared query parameter keys: - Added `paramRefCountMap` to track how many components are using each parameter key - Increment reference count when components mount - Decrement on unmount and only clean up when the last component using a key unmounts - This preserves the URL as the Single Source of Truth while preventing state leakage between pages ## Impact - Fixes unexpected state persistence across page navigation - Maintains proper state isolation between different pages - Preserves correct behavior when multiple components share the same query parameter keys [FR-1401]: https://lablup.atlassian.net/browse/FR-1401?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 3dd6515 commit 29e5c64

File tree

1 file changed

+50
-3
lines changed

1 file changed

+50
-3
lines changed

react/src/hooks/useDeferredQueryParams.tsx

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { atom, useAtomValue, useSetAtom } from 'jotai';
22
import { atomWithDefault } from 'jotai/utils';
33
import _ from 'lodash';
4-
import { useRef, useCallback, useMemo } from 'react';
4+
import { useRef, useCallback, useMemo, useEffect } from 'react';
55
import {
66
useQueryParams,
77
QueryParamConfigMap,
@@ -11,6 +11,9 @@ import {
1111

1212
const queryParamsAtom = atom<Record<string, any>>({});
1313

14+
// Reference counting atom for thread-safe tracking of parameter key usage
15+
const paramRefCountAtom = atom<Map<string, number>>(new Map());
16+
1417
/**
1518
* A custom hook that synchronizes URL search parameters with application state while handling React transitions.
1619
* This hook solves the issue where URL parameter changes within React transitions are not properly reflected
@@ -42,6 +45,51 @@ export function useDeferredQueryParams<QPCMap extends QueryParamConfigMap>(
4245
paramConfigMap: QPCMap,
4346
) {
4447
const [query, setQuery] = useQueryParams(paramConfigMap);
48+
const setSharedQuery = useSetAtom(queryParamsAtom);
49+
const setParamRefCount = useSetAtom(paramRefCountAtom);
50+
51+
const stringifiedParamConfigMap = useMemo(() => {
52+
return JSON.stringify(paramConfigMap);
53+
}, [paramConfigMap]);
54+
55+
// Reference counting based cleanup: only remove params when last component unmounts
56+
useEffect(() => {
57+
const keys = Object.keys(paramConfigMap);
58+
59+
// Mount: increase reference count for each key
60+
setParamRefCount((prev) => {
61+
const newMap = new Map(prev);
62+
keys.forEach((key) => {
63+
newMap.set(key, (newMap.get(key) || 0) + 1);
64+
});
65+
return newMap;
66+
});
67+
68+
return () => {
69+
// Unmount: decrease reference count and cleanup if necessary
70+
setParamRefCount((prev) => {
71+
const newMap = new Map(prev);
72+
keys.forEach((key) => {
73+
const currentCount = (newMap.get(key) || 0) - 1;
74+
75+
if (currentCount <= 0) {
76+
// Last component using this key is unmounting, safe to cleanup
77+
newMap.delete(key);
78+
setSharedQuery((queryPrev) => {
79+
const newState = { ...queryPrev };
80+
delete newState[key];
81+
return newState;
82+
});
83+
} else {
84+
// Other components still using this key
85+
newMap.set(key, currentCount);
86+
}
87+
});
88+
return newMap;
89+
});
90+
};
91+
// eslint-disable-next-line react-hooks/exhaustive-deps
92+
}, [stringifiedParamConfigMap, setParamRefCount, setSharedQuery]);
4593

4694
const isBeforeInitializingRef = useRef(true);
4795
const selectiveQueryAtom = useMemo(
@@ -67,11 +115,10 @@ export function useDeferredQueryParams<QPCMap extends QueryParamConfigMap>(
67115
});
68116
},
69117
// eslint-disable-next-line react-hooks/exhaustive-deps
70-
[JSON.stringify(paramConfigMap)],
118+
[stringifiedParamConfigMap],
71119
);
72120

73121
let localQuery = useAtomValue(selectiveQueryAtom);
74-
const setSharedQuery = useSetAtom(queryParamsAtom);
75122

76123
const setDeferredQuery = useCallback(
77124
(

0 commit comments

Comments
 (0)