From 109e840ea57f8244635f1e86a9fb93a9c41cd3da Mon Sep 17 00:00:00 2001 From: Jan-Willem Gmelig Meyling Date: Fri, 15 May 2026 15:16:22 +0200 Subject: [PATCH] feat(mui): add filters.syncFilterModel option to useDataGrid useDataGrid controls dataGridProps.filterModel from an internal muiCrudFilters state that is only updated by the DataGrid's own filter panel and by search(). Calls to the returned setFilters (or filter changes synced from the URL via syncWithLocation) updated filters but left filterModel stale, so apps driving filters from external UIs could not reflect their state in the grid. Add an opt-in filters.syncFilterModel flag. When true, a useEffect mirrors filters into muiCrudFilters via isEqual guard, so external mutations land in filterModel without a no-op re-render on the debounced internal path. The default is false to preserve the behavior agreed on in the original bug report, where unconditional syncing was rejected because it can clobber in-progress column-header edits in apps that mix DataGrid filter inputs and external setFilters calls. Refs: #5860 Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/quiet-otters-listen.md | 26 +++++++ .../mui/src/hooks/useDataGrid/index.spec.ts | 72 +++++++++++++++++++ packages/mui/src/hooks/useDataGrid/index.ts | 17 +++++ 3 files changed, 115 insertions(+) create mode 100644 .changeset/quiet-otters-listen.md diff --git a/.changeset/quiet-otters-listen.md b/.changeset/quiet-otters-listen.md new file mode 100644 index 0000000000000..8f9cba0f451e6 --- /dev/null +++ b/.changeset/quiet-otters-listen.md @@ -0,0 +1,26 @@ +--- +"@refinedev/mui": patch +--- + +feat: add `filters.syncFilterModel` option to `useDataGrid` + +`useDataGrid` controls `dataGridProps.filterModel` from an internal state that +was only updated by the DataGrid's own filter panel and by `search()`. As a +result, calls to the returned `setFilters` (or filter changes synced from the +URL via `syncWithLocation`) updated `filters` but left `filterModel` stale, so +external filter UIs could not drive the grid's visible filter state. + +Add an opt-in `filters.syncFilterModel` flag. When `true`, the hook reflects +`filters` changes in `dataGridProps.filterModel`. The default is `false` to +preserve compatibility with apps that mix column-header filtering and +external filter inputs — see #5860 for the original reasoning behind the +controlled-but-unsynced behavior. + +```tsx +const { dataGridProps } = useDataGrid({ + resource: "posts", + filters: { + syncFilterModel: true, + }, +}); +``` diff --git a/packages/mui/src/hooks/useDataGrid/index.spec.ts b/packages/mui/src/hooks/useDataGrid/index.spec.ts index e3eafa5f2831f..b9f99de1f255e 100644 --- a/packages/mui/src/hooks/useDataGrid/index.spec.ts +++ b/packages/mui/src/hooks/useDataGrid/index.spec.ts @@ -308,6 +308,78 @@ describe("useDataGrid Hook", () => { }); }); + it("when filters.syncFilterModel is true, external setFilters updates dataGridProps.filterModel", async () => { + const { result } = renderHook( + () => + useDataGrid({ + resource: "posts", + filters: { + syncFilterModel: true, + }, + }), + { + wrapper: TestWrapper({}), + }, + ); + + await waitFor(() => { + expect(!result.current.tableQuery?.isLoading).toBeTruthy(); + }); + + await act(async () => { + result.current.setFilters([ + { + field: "title", + operator: "contains", + value: "test", + }, + ]); + }); + + await waitFor(() => { + expect(result.current.dataGridProps.filterModel?.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + field: "title", + value: "test", + }), + ]), + ); + }); + }); + + it("by default, external setFilters does not update dataGridProps.filterModel", async () => { + const { result } = renderHook( + () => + useDataGrid({ + resource: "posts", + }), + { + wrapper: TestWrapper({}), + }, + ); + + await waitFor(() => { + expect(!result.current.tableQuery?.isLoading).toBeTruthy(); + }); + + const initialItems = result.current.dataGridProps.filterModel?.items; + + await act(async () => { + result.current.setFilters([ + { + field: "title", + operator: "contains", + value: "test", + }, + ]); + }); + + expect(result.current.dataGridProps.filterModel?.items).toEqual( + initialItems, + ); + }); + it("should not change sortModel when page changes", async () => { const { result } = renderHook( () => diff --git a/packages/mui/src/hooks/useDataGrid/index.ts b/packages/mui/src/hooks/useDataGrid/index.ts index 2a89c85468bee..5b0c06081d57d 100644 --- a/packages/mui/src/hooks/useDataGrid/index.ts +++ b/packages/mui/src/hooks/useDataGrid/index.ts @@ -90,6 +90,16 @@ export type UseDataGridProps< * @default "replace" */ defaultBehavior?: "replace" | "merge"; + /** + * When `true`, changes to `filters` made outside the DataGrid (via the + * returned `setFilters`, `search`, or `syncWithLocation`) are reflected + * in `dataGridProps.filterModel`. Defaults to `false` to preserve + * compatibility with apps that mix DataGrid column-header filtering and + * external filter inputs, where forcing a sync would clobber in-progress + * header edits. + * @default false + */ + syncFilterModel?: boolean; } >; editable?: boolean; @@ -200,6 +210,13 @@ export function useDataGrid< const [muiCrudFilters, setMuiCrudFilters] = useState(filters); + const syncFilterModel = filtersFromProp?.syncFilterModel ?? false; + + useEffect(() => { + if (!syncFilterModel) return; + setMuiCrudFilters((prev) => (isEqual(prev, filters) ? prev : filters)); + }, [filters, syncFilterModel]); + const { data, isFetched, isLoading } = tableQuery; const rowCountRef = useRef(data?.total || 0);