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, ] );