From 4dd34fcd8fffc49ce919c821051d47b022e214d9 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Wed, 5 Nov 2025 11:54:46 +0100
Subject: [PATCH 1/5] Improve lists URL management
---
.../controller/list/useListParams.spec.tsx | 123 ++++++++++++++++++
.../src/controller/list/useListParams.ts | 20 +++
2 files changed, 143 insertions(+)
diff --git a/packages/ra-core/src/controller/list/useListParams.spec.tsx b/packages/ra-core/src/controller/list/useListParams.spec.tsx
index cc9c1c0b9dc..7d8b9245d38 100644
--- a/packages/ra-core/src/controller/list/useListParams.spec.tsx
+++ b/packages/ra-core/src/controller/list/useListParams.spec.tsx
@@ -9,6 +9,7 @@ import { useStore } from '../../store/useStore';
import { useListParams, getQuery, getNumberOrDefault } from './useListParams';
import { SORT_DESC, SORT_ASC } from './queryReducer';
import { TestMemoryRouter } from '../../routing';
+import { memoryStore } from '../../store';
describe('useListParams', () => {
describe('getQuery', () => {
@@ -495,6 +496,71 @@ describe('useListParams', () => {
});
});
+ it('should synchronize location with store when sync is enabled', async () => {
+ let location;
+ let storeValue;
+ const StoreReader = () => {
+ const [value] = useStore('posts.listParams');
+ React.useEffect(() => {
+ storeValue = value;
+ }, [value]);
+ return null;
+ };
+ render(
+ {
+ location = l;
+ }}
+ >
+
+
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(storeValue).toEqual({
+ sort: 'id',
+ order: 'ASC',
+ page: 10,
+ perPage: 10,
+ filter: {},
+ });
+ });
+
+ await waitFor(() => {
+ expect(location).toEqual(
+ expect.objectContaining({
+ hash: '',
+ key: expect.any(String),
+ state: null,
+ pathname: '/',
+ search:
+ '?' +
+ stringify({
+ filter: JSON.stringify({}),
+ sort: 'id',
+ order: 'ASC',
+ page: 10,
+ perPage: 10,
+ }),
+ })
+ );
+ });
+ });
+
it('should not synchronize parameters with location and store when sync is not enabled', async () => {
let location;
let storeValue;
@@ -540,6 +606,63 @@ describe('useListParams', () => {
expect(storeValue).toBeUndefined();
});
+ it('should not synchronize location with store when sync is not enabled', async () => {
+ let location;
+ let storeValue;
+ const StoreReader = () => {
+ const [value] = useStore('posts.listParams');
+ React.useEffect(() => {
+ storeValue = value;
+ }, [value]);
+ return null;
+ };
+ render(
+ {
+ location = l;
+ }}
+ >
+
+
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(storeValue).toEqual({
+ sort: 'id',
+ order: 'ASC',
+ page: 10,
+ perPage: 10,
+ filter: {},
+ });
+ });
+
+ await waitFor(() => {
+ expect(location).toEqual(
+ expect.objectContaining({
+ hash: '',
+ key: expect.any(String),
+ state: null,
+ pathname: '/',
+ search: '',
+ })
+ );
+ });
+ });
+
it('should synchronize parameters with store when sync is not enabled and storeKey is passed', async () => {
let storeValue;
const Component = ({
diff --git a/packages/ra-core/src/controller/list/useListParams.ts b/packages/ra-core/src/controller/list/useListParams.ts
index eca5484aed5..7d3cdf949bf 100644
--- a/packages/ra-core/src/controller/list/useListParams.ts
+++ b/packages/ra-core/src/controller/list/useListParams.ts
@@ -1,6 +1,7 @@
import { useCallback, useMemo, useEffect, useState, useRef } from 'react';
import { parse, stringify } from 'query-string';
import lodashDebounce from 'lodash/debounce.js';
+import isEqual from 'lodash/isEqual.js';
import { useNavigate, useLocation } from 'react-router-dom';
import { useStore } from '../../store';
@@ -133,6 +134,25 @@ export const useListParams = ({
}
}, [location.search]); // eslint-disable-line
+ // 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 (disableSyncWithLocation) {
+ return;
+ }
+ if (!isEqual(query, queryFromLocation)) {
+ navigate({
+ search: `?${stringify({
+ ...query,
+ filter: JSON.stringify(query.filter),
+ displayedFilters: JSON.stringify(query.displayedFilters),
+ })}`,
+ });
+ }
+ }, [disableSyncWithLocation, query, location.search]); // eslint-disable-line
+
const changeParams = useCallback(
action => {
// do not change params if the component is already unmounted
From 4e70e17904acf78232fd09f8a3c052b62658a087 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Wed, 5 Nov 2025 12:01:38 +0100
Subject: [PATCH 2/5] Don't update location if it has parameters
---
.../controller/list/useListParams.spec.tsx | 78 +++++++++++++++++++
.../src/controller/list/useListParams.ts | 5 +-
2 files changed, 82 insertions(+), 1 deletion(-)
diff --git a/packages/ra-core/src/controller/list/useListParams.spec.tsx b/packages/ra-core/src/controller/list/useListParams.spec.tsx
index 7d8b9245d38..317182f1804 100644
--- a/packages/ra-core/src/controller/list/useListParams.spec.tsx
+++ b/packages/ra-core/src/controller/list/useListParams.spec.tsx
@@ -606,6 +606,84 @@ describe('useListParams', () => {
expect(storeValue).toBeUndefined();
});
+ 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(
+ {
+ location = l;
+ }}
+ >
+
+
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(storeValue).toEqual({
+ sort: 'id',
+ order: 'ASC',
+ page: 10,
+ perPage: 10,
+ filter: {},
+ });
+ });
+
+ await waitFor(() => {
+ expect(location).toEqual(
+ expect.objectContaining({
+ hash: '',
+ key: expect.any(String),
+ state: null,
+ pathname: '/',
+ search:
+ '?' +
+ stringify({
+ filter: JSON.stringify({}),
+ sort: 'id',
+ order: 'ASC',
+ page: 5,
+ perPage: 10,
+ }),
+ })
+ );
+ });
+ });
+
it('should not synchronize location with store when sync is not enabled', async () => {
let location;
let storeValue;
diff --git a/packages/ra-core/src/controller/list/useListParams.ts b/packages/ra-core/src/controller/list/useListParams.ts
index 7d3cdf949bf..4cdf302e0f5 100644
--- a/packages/ra-core/src/controller/list/useListParams.ts
+++ b/packages/ra-core/src/controller/list/useListParams.ts
@@ -142,7 +142,10 @@ export const useListParams = ({
if (disableSyncWithLocation) {
return;
}
- if (!isEqual(query, queryFromLocation)) {
+ if (
+ !isEqual(query, queryFromLocation) &&
+ Object.keys(queryFromLocation).length === 0
+ ) {
navigate({
search: `?${stringify({
...query,
From 59a4bd2549262356b4f6309cc336a61bb27117de Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Wed, 5 Nov 2025 14:19:36 +0100
Subject: [PATCH 3/5] Improve useListParams to not update the URL when params
are the default
---
.../src/controller/list/useListParams.ts | 44 ++++++++++++++-----
1 file changed, 34 insertions(+), 10 deletions(-)
diff --git a/packages/ra-core/src/controller/list/useListParams.ts b/packages/ra-core/src/controller/list/useListParams.ts
index 4cdf302e0f5..91ff348a34a 100644
--- a/packages/ra-core/src/controller/list/useListParams.ts
+++ b/packages/ra-core/src/controller/list/useListParams.ts
@@ -138,14 +138,28 @@ 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 (disableSyncWithLocation) {
- return;
- }
- if (
- !isEqual(query, queryFromLocation) &&
- Object.keys(queryFromLocation).length === 0
- ) {
+ useEffect(
+ () => {
+ 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,
@@ -153,8 +167,18 @@ export const useListParams = ({
displayedFilters: JSON.stringify(query.displayedFilters),
})}`,
});
- }
- }, [disableSyncWithLocation, query, location.search]); // eslint-disable-line
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [
+ navigate,
+ disableSyncWithLocation,
+ filterDefaultValues,
+ perPage,
+ sort,
+ query,
+ location.search,
+ ]
+ );
const changeParams = useCallback(
action => {
From 467446f0afa0445ca4fc84f3ec6d3700e3965d37 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Wed, 5 Nov 2025 14:29:57 +0100
Subject: [PATCH 4/5] Add a test for default params handling
---
.../controller/list/useListParams.spec.tsx | 30 +++++++++++++++++++
1 file changed, 30 insertions(+)
diff --git a/packages/ra-core/src/controller/list/useListParams.spec.tsx b/packages/ra-core/src/controller/list/useListParams.spec.tsx
index 317182f1804..7d2d1f030d0 100644
--- a/packages/ra-core/src/controller/list/useListParams.spec.tsx
+++ b/packages/ra-core/src/controller/list/useListParams.spec.tsx
@@ -684,6 +684,36 @@ describe('useListParams', () => {
});
});
+ it('should not synchronize location with store if the store parameters are the defaults', async () => {
+ let location;
+ render(
+ {
+ location = l;
+ }}
+ >
+
+
+
+
+ );
+
+ // 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 when sync is not enabled', async () => {
let location;
let storeValue;
From 6bf1e23f6c9409f3c605a05ddb5e7f1983de6003 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Wed, 5 Nov 2025 14:55:10 +0100
Subject: [PATCH 5/5] Fix issues with same resource, different store keys lists
---
packages/ra-core/src/controller/list/useListParams.ts | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/packages/ra-core/src/controller/list/useListParams.ts b/packages/ra-core/src/controller/list/useListParams.ts
index 91ff348a34a..1ae50586515 100644
--- a/packages/ra-core/src/controller/list/useListParams.ts
+++ b/packages/ra-core/src/controller/list/useListParams.ts
@@ -134,12 +134,20 @@ 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.
+ if (currentStoreKey.current !== storeKey) {
+ // storeKey has changed
+ currentStoreKey.current = storeKey;
+ return;
+ }
if (disableSyncWithLocation) {
return;
}
@@ -150,6 +158,7 @@ export const useListParams = ({
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 ||
@@ -177,6 +186,7 @@ export const useListParams = ({
sort,
query,
location.search,
+ params,
]
);