) => {
+ const [{ page, perPage, sort, order, filter }, { setPage }] =
+ useListParams({
+ resource: 'posts',
+ disableSyncWithLocation,
+ ...options,
+ });
const handleClick = () => {
setPage(10);
@@ -374,6 +384,10 @@ describe('useListParams', () => {
return (
<>
page: {page}
+ perPage: {perPage}
+ sort: {sort}
+ order: {order}
+ filter: {JSON.stringify(filter)}
>
);
@@ -608,14 +622,6 @@ describe('useListParams', () => {
it('should not synchronize location with store if the location already contains parameters', async () => {
let location;
- let storeValue;
- const StoreReader = () => {
- const [value] = useStore('posts.listParams');
- React.useEffect(() => {
- storeValue = value;
- }, [value]);
- return null;
- };
render(
{
},
})}
>
-
-
+
);
- await waitFor(() => {
- expect(storeValue).toEqual({
- sort: 'id',
- order: 'ASC',
- page: 10,
- perPage: 10,
- filter: {},
- });
- });
-
await waitFor(() => {
expect(location).toEqual(
expect.objectContaining({
@@ -693,7 +688,40 @@ describe('useListParams', () => {
}}
>
-
+
+
+
+ );
+
+ // Let React do its thing
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ await waitFor(() => {
+ expect(location).toEqual(
+ expect.objectContaining({
+ hash: '',
+ key: expect.any(String),
+ state: null,
+ pathname: '/',
+ search: '',
+ })
+ );
+ });
+ });
+
+ it('should not synchronize location with store if the store parameters are the custom defaults provided to the hook', async () => {
+ let location;
+ render(
+ {
+ location = l;
+ }}
+ >
+
+
);
@@ -701,6 +729,12 @@ describe('useListParams', () => {
// Let React do its thing
await new Promise(resolve => setTimeout(resolve, 0));
+ // The list is using the default set on the component
+ await screen.findByText('perPage: 5');
+ await screen.findByText('sort: title');
+ await screen.findByText('order: DESC');
+
+ // The location is the default for the list (no query parameters)
await waitFor(() => {
expect(location).toEqual(
expect.objectContaining({
diff --git a/packages/ra-core/src/controller/list/useListParams.ts b/packages/ra-core/src/controller/list/useListParams.ts
index 8755a98e76f..7e428ef636f 100644
--- a/packages/ra-core/src/controller/list/useListParams.ts
+++ b/packages/ra-core/src/controller/list/useListParams.ts
@@ -107,9 +107,10 @@ export const useListParams = ({
disableSyncWithLocation,
];
- const queryFromLocation = disableSyncWithLocation
- ? {}
- : parseQueryFromLocation(location);
+ const queryFromLocation = useMemo(
+ () => (disableSyncWithLocation ? {} : parseQueryFromLocation(location)),
+ [location, disableSyncWithLocation]
+ );
const query = useMemo(
() =>
@@ -138,63 +139,59 @@ export const useListParams = ({
// the categories products on the demo), we need to persist them in the
// store as well so that we don't lose them after a redirection back
// to the list
- useEffect(
- () => {
- // If the storeKey has changed, ignore the first effect call. This avoids conflicts between lists sharing
- // the same resource but different storeKeys.
- if (currentStoreKey.current !== storeKey) {
- // storeKey has changed
- currentStoreKey.current = storeKey;
- return;
- }
- if (disableSyncWithLocation) {
- return;
- }
- const defaultParams = {
- filter: filterDefaultValues || {},
- page: 1,
- perPage,
- sort: sort.field,
- order: sort.order,
- };
-
- if (
- // The location params are not empty (we don't want to override them if provided)
- Object.keys(queryFromLocation).length > 0 ||
- // or the stored params are different from the location params
- isEqual(query, queryFromLocation) ||
- // or the stored params are not different from the default params (to keep the URL simple when possible)
- isEqual(query, defaultParams)
- ) {
- return;
- }
- navigate(
- {
- search: `?${stringify({
- ...query,
- filter: JSON.stringify(query.filter),
- displayedFilters: JSON.stringify(
- query.displayedFilters
- ),
- })}`,
- },
- {
- replace: true,
- }
- );
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [
- navigate,
- disableSyncWithLocation,
- filterDefaultValues,
+ useEffect(() => {
+ // If the storeKey has changed, ignore the first effect call. This avoids conflicts between lists sharing
+ // the same resource but different storeKeys.
+ if (currentStoreKey.current !== storeKey) {
+ // storeKey has changed
+ currentStoreKey.current = storeKey;
+ return;
+ }
+ if (disableSyncWithLocation) {
+ return;
+ }
+ const defaultParams = {
+ filter: filterDefaultValues || {},
+ page: 1,
perPage,
- sort,
- query,
- location.search,
- params,
- ]
- );
+ sort: sort.field,
+ order: sort.order,
+ };
+
+ if (
+ // The location params are not empty (we don't want to override them if provided)
+ Object.keys(queryFromLocation).length > 0 ||
+ // or the stored params are the same as the location params
+ isEqual(query, queryFromLocation) ||
+ // or the stored params are the same as the default params (to keep the URL simple when possible)
+ isEqual(query, defaultParams)
+ ) {
+ return;
+ }
+ navigate(
+ {
+ search: `?${stringify({
+ ...query,
+ filter: JSON.stringify(query.filter),
+ displayedFilters: JSON.stringify(query.displayedFilters),
+ })}`,
+ },
+ {
+ replace: true,
+ }
+ );
+ }, [
+ navigate,
+ disableSyncWithLocation,
+ filterDefaultValues,
+ perPage,
+ sort,
+ query,
+ queryFromLocation,
+ location.search,
+ params,
+ storeKey,
+ ]);
const changeParams = useCallback(
action => {
@@ -340,7 +337,7 @@ const parseObject = (query, field) => {
if (query[field] && typeof query[field] === 'string') {
try {
query[field] = JSON.parse(query[field]);
- } catch (err) {
+ } catch {
delete query[field];
}
}
From e19040eee81b9e91807f9f8b3ec89c717f9a79c4 Mon Sep 17 00:00:00 2001
From: React-Admin CI
Date: Wed, 4 Feb 2026 11:04:46 +0100
Subject: [PATCH 8/9] Ensure default `displayedFilters` are ignored too
---
packages/ra-core/src/controller/list/useListParams.ts | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/packages/ra-core/src/controller/list/useListParams.ts b/packages/ra-core/src/controller/list/useListParams.ts
index 7e428ef636f..21247941953 100644
--- a/packages/ra-core/src/controller/list/useListParams.ts
+++ b/packages/ra-core/src/controller/list/useListParams.ts
@@ -135,10 +135,6 @@ export const useListParams = ({
}, [location.search]); // eslint-disable-line
const currentStoreKey = useRef(storeKey);
- // if the location includes params (for example from a link like
- // the categories products on the demo), we need to persist them in the
- // store as well so that we don't lose them after a redirection back
- // to the list
useEffect(() => {
// If the storeKey has changed, ignore the first effect call. This avoids conflicts between lists sharing
// the same resource but different storeKeys.
@@ -151,6 +147,7 @@ export const useListParams = ({
return;
}
const defaultParams = {
+ displayedFilters: {},
filter: filterDefaultValues || {},
page: 1,
perPage,
@@ -168,6 +165,7 @@ export const useListParams = ({
) {
return;
}
+ console.log({ query, queryFromLocation, defaultParams });
navigate(
{
search: `?${stringify({
From cdba26c2bbd6969575e5444d522b2240b0ff7aaa Mon Sep 17 00:00:00 2001
From: React-Admin CI
Date: Wed, 4 Feb 2026 11:51:00 +0100
Subject: [PATCH 9/9] Fix useListParams
---
packages/ra-core/src/controller/list/useListParams.ts | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/packages/ra-core/src/controller/list/useListParams.ts b/packages/ra-core/src/controller/list/useListParams.ts
index 21247941953..3f92cbbd7d8 100644
--- a/packages/ra-core/src/controller/list/useListParams.ts
+++ b/packages/ra-core/src/controller/list/useListParams.ts
@@ -147,7 +147,6 @@ export const useListParams = ({
return;
}
const defaultParams = {
- displayedFilters: {},
filter: filterDefaultValues || {},
page: 1,
perPage,
@@ -155,17 +154,21 @@ export const useListParams = ({
order: sort.order,
};
+ const {
+ displayedFilters: _displayedFilters,
+ ...queryWithoutDisplayedFilters
+ } = query;
+
if (
// The location params are not empty (we don't want to override them if provided)
Object.keys(queryFromLocation).length > 0 ||
// or the stored params are the same as the location params
- isEqual(query, queryFromLocation) ||
+ isEqual(queryWithoutDisplayedFilters, queryFromLocation) ||
// or the stored params are the same as the default params (to keep the URL simple when possible)
- isEqual(query, defaultParams)
+ isEqual(queryWithoutDisplayedFilters, defaultParams)
) {
return;
}
- console.log({ query, queryFromLocation, defaultParams });
navigate(
{
search: `?${stringify({