Skip to content

Commit 71272b8

Browse files
Optimize useDataTableUrlState with memoization
Co-authored-by: jaruesink <4207065+jaruesink@users.noreply.github.com>
1 parent 33ac356 commit 71272b8

8 files changed

Lines changed: 1313 additions & 19 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
2+
import { performance } from 'perf_hooks';
3+
import { dataTableRouterParsers } from '../src/remix-hook-form/data-table-router-parsers';
4+
5+
const ITERATIONS = 10000;
6+
7+
// Create a complex filter state serialized to JSON
8+
const complexFilters = [
9+
{ columnId: 'name', type: 'text', operator: 'contains', values: ['John'] },
10+
{ columnId: 'age', type: 'number', operator: 'between', values: [20, 30] },
11+
{ columnId: 'status', type: 'select', operator: 'isAnyOf', values: ['active', 'pending'] },
12+
{ columnId: 'role', type: 'text', operator: 'equals', values: ['admin'] },
13+
{ columnId: 'department', type: 'text', operator: 'contains', values: ['engineering'] },
14+
];
15+
16+
const serializedFilters = JSON.stringify(complexFilters);
17+
18+
function runBenchmark() {
19+
console.log('Running benchmark...');
20+
const start = performance.now();
21+
22+
for (let i = 0; i < ITERATIONS; i++) {
23+
// Simulate the parsing operation that happens in the hook
24+
dataTableRouterParsers.filters.parse(serializedFilters);
25+
}
26+
27+
const end = performance.now();
28+
const totalTime = end - start;
29+
const avgTime = totalTime / ITERATIONS;
30+
31+
console.log(`Total time for ${ITERATIONS} iterations: ${totalTime.toFixed(2)}ms`);
32+
console.log(`Average time per parse: ${avgTime.toFixed(4)}ms`);
33+
}
34+
35+
runBenchmark();

packages/components/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,19 +86,23 @@
8686
"devDependencies": {
8787
"@react-router/dev": "^7.0.0",
8888
"@react-router/node": "^7.0.0",
89+
"@testing-library/jest-dom": "^6.9.1",
90+
"@testing-library/react": "^16.3.2",
8991
"@types/glob": "^8.1.0",
9092
"@types/react": "^19.0.0",
9193
"@typescript-eslint/eslint-plugin": "^6.21.0",
9294
"@typescript-eslint/parser": "^6.21.0",
9395
"@vitejs/plugin-react": "^4.3.4",
9496
"autoprefixer": "^10.4.20",
9597
"glob": "^11.0.0",
98+
"jsdom": "^28.0.0",
9699
"react": "^19.0.0",
97100
"tailwindcss": "^4.0.0",
98101
"typescript": "^5.7.2",
99102
"vite": "^6.2.2",
100103
"vite-plugin-dts": "^4.4.0",
101104
"vite-tsconfig-paths": "^5.1.4",
105+
"vitest": "^4.0.18",
102106
"zod": "^3.24.1"
103107
}
104108
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { renderHook, act } from '@testing-library/react';
2+
import { MemoryRouter, useLocation } from 'react-router';
3+
import { describe, it, expect } from 'vitest';
4+
import { useDataTableUrlState } from './use-data-table-url-state';
5+
import React from 'react';
6+
7+
describe('useDataTableUrlState', () => {
8+
it('should parse initial state from URL', () => {
9+
const wrapper = ({ children }: { children: React.ReactNode }) => (
10+
<MemoryRouter initialEntries={['/?search=initial&page=2&pageSize=20']}>
11+
{children}
12+
</MemoryRouter>
13+
);
14+
15+
const { result } = renderHook(() => useDataTableUrlState(), { wrapper });
16+
17+
expect(result.current.urlState.search).toBe('initial');
18+
expect(result.current.urlState.page).toBe(2);
19+
expect(result.current.urlState.pageSize).toBe(20);
20+
});
21+
22+
it('should update URL state', () => {
23+
let testLocation: any;
24+
25+
const LocationSpy = () => {
26+
testLocation = useLocation();
27+
return null;
28+
};
29+
30+
const wrapper = ({ children }: { children: React.ReactNode }) => (
31+
<MemoryRouter initialEntries={['/']}>
32+
<LocationSpy />
33+
{children}
34+
</MemoryRouter>
35+
);
36+
37+
const { result } = renderHook(() => useDataTableUrlState(), { wrapper });
38+
39+
act(() => {
40+
result.current.setUrlState({ search: 'updated' });
41+
});
42+
43+
expect(result.current.urlState.search).toBe('updated');
44+
expect(testLocation.search).toContain('search=updated');
45+
});
46+
});

packages/components/src/remix-hook-form/use-data-table-url-state.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback } from 'react';
1+
import { useCallback, useMemo } from 'react';
22
import { useSearchParams } from 'react-router';
33
import { type DataTableRouterState, dataTableRouterParsers } from './data-table-router-parsers';
44

@@ -14,18 +14,20 @@ export function useDataTableUrlState() {
1414
const [searchParams, setSearchParams] = useSearchParams();
1515

1616
// Parse URL search parameters using our custom parsers
17-
const urlState: DataTableRouterState = {
18-
search: dataTableRouterParsers.search.parse(searchParams.get('search')),
19-
filters: dataTableRouterParsers.filters.parse(searchParams.get('filters')),
20-
page: dataTableRouterParsers.page.parse(searchParams.get('page')),
21-
pageSize: dataTableRouterParsers.pageSize.parse(searchParams.get('pageSize')),
22-
sortField: dataTableRouterParsers.sortField.parse(searchParams.get('sortField')),
23-
// 'asc' or 'desc'
24-
sortOrder: dataTableRouterParsers.sortOrder.parse(searchParams.get('sortOrder')),
25-
};
17+
const urlState: DataTableRouterState = useMemo(
18+
() => ({
19+
search: dataTableRouterParsers.search.parse(searchParams.get('search')),
20+
filters: dataTableRouterParsers.filters.parse(searchParams.get('filters')),
21+
page: dataTableRouterParsers.page.parse(searchParams.get('page')),
22+
pageSize: dataTableRouterParsers.pageSize.parse(searchParams.get('pageSize')),
23+
sortField: dataTableRouterParsers.sortField.parse(searchParams.get('sortField')),
24+
// 'asc' or 'desc'
25+
sortOrder: dataTableRouterParsers.sortOrder.parse(searchParams.get('sortOrder')),
26+
}),
27+
[searchParams],
28+
);
2629

2730
// Function to update URL search parameters
28-
// biome-ignore lint/correctness/useExhaustiveDependencies: setSearchParams is stable; urlState is read at call time
2931
const setUrlState = useCallback(
3032
(newState: Partial<DataTableRouterState>) => {
3133
const updatedState = { ...urlState, ...newState };
@@ -65,7 +67,7 @@ export function useDataTableUrlState() {
6567
// Update the URL with the new search parameters
6668
setSearchParams(newParams, { replace: true });
6769
},
68-
[setSearchParams],
70+
[setSearchParams, urlState],
6971
);
7072

7173
// Return the current URL state and the function to update it
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import '@testing-library/jest-dom';

packages/components/vite.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export default defineConfig({
2828
.sync('src/**/*.{ts,tsx}', {
2929
ignore: [
3030
'src/**/*.d.ts',
31+
'src/**/*.test.tsx',
32+
'src/**/*.test.ts',
33+
'src/**/test/**',
3134
'src/**/core/types.ts', // Exclude type-only files to avoid empty chunks
3235
],
3336
})
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/// <reference types="vitest" />
2+
import { defineConfig } from 'vite';
3+
import react from '@vitejs/plugin-react';
4+
5+
export default defineConfig({
6+
plugins: [react()],
7+
test: {
8+
globals: true,
9+
environment: 'jsdom',
10+
setupFiles: ['./src/test/setup.ts'],
11+
},
12+
});

0 commit comments

Comments
 (0)