Skip to content

Commit 5405181

Browse files
committed
refactor: rename synchronizer to persistence
- CollectionStateSynchronizer → CollectionStatePersistence - useSearchParamsSynchronizer → useSearchParamsPersistence - Simplify to read()/write() pattern (no subscribe) - Inline use-collection-state into use-collection-variables - Remove react-router dependency from persistence layer
1 parent 12ba145 commit 5405181

9 files changed

Lines changed: 458 additions & 626 deletions

examples/nextjs-app/src/modules/pages/data-table-demo.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
createColumnHelper,
77
Layout,
88
type RowAction,
9+
useSearchParamsPersistence,
910
} from "@tailor-platform/app-shell";
1011
import { useState } from "react";
1112
import { type Product, useProductsQuery } from "./mock-data";
@@ -140,9 +141,11 @@ const productRowActions: RowAction<Product>[] = [
140141
// ---------------------------------------------------------------------------
141142

142143
const DataTableDemoPage = () => {
144+
const persistence = useSearchParamsPersistence({ prefix: "dt1" });
143145
const { variables, control } = useCollectionVariables({
144146
params: { pageSize: 5 },
145147
tableMetadata: productMetadata,
148+
persistence,
146149
});
147150
const { data, loading } = useProductsQuery(variables);
148151
const [selectedIds, setSelectedIds] = useState<string[]>([]);

packages/core/src/hooks/use-collection-state.ts

Lines changed: 0 additions & 145 deletions
This file was deleted.

packages/core/src/hooks/use-collection-variables-synchronizer.test.ts renamed to packages/core/src/hooks/use-collection-variables-persistence.test.ts

Lines changed: 30 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,19 @@
11
import { renderHook, act } from "@testing-library/react";
22
import { describe, it, expect, vi } from "vitest";
3-
import type { CollectionSnapshot, CollectionStateSynchronizer } from "@/types/collection";
3+
import type { CollectionSnapshot, CollectionStatePersistence } from "@/types/collection";
44
import { useCollectionVariables } from "./use-collection-variables";
55

6-
describe("useCollectionVariables with synchronizer", () => {
7-
function createMockSynchronizer(initial?: CollectionSnapshot) {
8-
const subscribers: Array<(snapshot: CollectionSnapshot | undefined) => void> = [];
6+
describe("useCollectionVariables with persistence", () => {
7+
function createMockPersistence(initial?: CollectionSnapshot) {
98
return {
10-
subscribe: vi.fn((onChange) => {
11-
subscribers.push(onChange);
12-
// BehaviorSubject: emit immediately
13-
onChange(initial);
14-
return () => {
15-
const idx = subscribers.indexOf(onChange);
16-
if (idx >= 0) subscribers.splice(idx, 1);
17-
};
18-
}),
9+
read: vi.fn(() => initial),
1910
write: vi.fn(),
20-
// Test helper to simulate external change
21-
emit(snapshot: CollectionSnapshot | undefined) {
22-
for (const cb of subscribers) cb(snapshot);
23-
},
24-
} satisfies CollectionStateSynchronizer & {
25-
emit: (s: CollectionSnapshot | undefined) => void;
26-
};
11+
} satisfies CollectionStatePersistence;
2712
}
2813

29-
describe("subscribe (initial hydration)", () => {
30-
it("uses synchronizer initial state over params defaults", () => {
31-
const synchronizer = createMockSynchronizer({
14+
describe("read (initial hydration)", () => {
15+
it("uses persistence initial state over params defaults", () => {
16+
const persistence = createMockPersistence({
3217
filters: [{ field: "status", operator: "eq", value: "ACTIVE" }],
3318
sort: [{ field: "name", direction: "Asc" }],
3419
pageSize: 50,
@@ -37,28 +22,28 @@ describe("useCollectionVariables with synchronizer", () => {
3722
const { result } = renderHook(() =>
3823
useCollectionVariables({
3924
params: { pageSize: 20 },
40-
synchronizer,
25+
persistence,
4126
}),
4227
);
4328

44-
expect(synchronizer.subscribe).toHaveBeenCalledOnce();
29+
expect(persistence.read).toHaveBeenCalledOnce();
4530
expect(result.current.control.filters).toEqual([
4631
{ field: "status", operator: "eq", value: "ACTIVE" },
4732
]);
4833
expect(result.current.control.sortStates).toEqual([{ field: "name", direction: "Asc" }]);
4934
expect(result.current.control.pageSize).toBe(50);
5035
});
5136

52-
it("falls back to params when synchronizer emits undefined", () => {
53-
const synchronizer = createMockSynchronizer(undefined);
37+
it("falls back to params when persistence returns undefined", () => {
38+
const persistence = createMockPersistence(undefined);
5439

5540
const { result } = renderHook(() =>
5641
useCollectionVariables({
5742
params: {
5843
pageSize: 30,
5944
initialSort: [{ field: "createdAt", direction: "Desc" }],
6045
},
61-
synchronizer,
46+
persistence,
6247
}),
6348
);
6449

@@ -69,8 +54,8 @@ describe("useCollectionVariables with synchronizer", () => {
6954
expect(result.current.control.filters).toEqual([]);
7055
});
7156

72-
it("partially overrides params (only pageSize from synchronizer)", () => {
73-
const synchronizer = createMockSynchronizer({
57+
it("partially overrides params (only pageSize from persistence)", () => {
58+
const persistence = createMockPersistence({
7459
pageSize: 100,
7560
});
7661

@@ -80,73 +65,48 @@ describe("useCollectionVariables with synchronizer", () => {
8065
pageSize: 20,
8166
initialSort: [{ field: "name", direction: "Asc" }],
8267
},
83-
synchronizer,
68+
persistence,
8469
}),
8570
);
8671

8772
expect(result.current.control.pageSize).toBe(100);
88-
// Sort remains from params since synchronizer didn't provide it
73+
// Sort remains from params since persistence didn't provide it
8974
expect(result.current.control.sortStates).toEqual([{ field: "name", direction: "Asc" }]);
9075
});
9176
});
9277

93-
describe("subscribe (external changes)", () => {
94-
it("updates state when synchronizer emits external change", () => {
95-
const synchronizer = createMockSynchronizer(undefined);
96-
97-
const { result } = renderHook(() =>
98-
useCollectionVariables({
99-
params: { pageSize: 20 },
100-
synchronizer,
101-
}),
102-
);
103-
104-
act(() => {
105-
synchronizer.emit({
106-
filters: [{ field: "name", operator: "contains", value: "test" }],
107-
pageSize: 50,
108-
});
109-
});
110-
111-
expect(result.current.control.filters).toEqual([
112-
{ field: "name", operator: "contains", value: "test" },
113-
]);
114-
expect(result.current.control.pageSize).toBe(50);
115-
});
116-
});
117-
11878
describe("write (state persistence)", () => {
11979
it("does not call write on initial mount (skip first write)", () => {
120-
const synchronizer = createMockSynchronizer({
80+
const persistence = createMockPersistence({
12181
filters: [{ field: "status", operator: "eq", value: "ACTIVE" }],
12282
pageSize: 50,
12383
});
12484

12585
renderHook(() =>
12686
useCollectionVariables({
12787
params: { pageSize: 20 },
128-
synchronizer,
88+
persistence,
12989
}),
13090
);
13191

132-
expect(synchronizer.write).not.toHaveBeenCalled();
92+
expect(persistence.write).not.toHaveBeenCalled();
13393
});
13494

13595
it("calls write on filter change", () => {
136-
const synchronizer = createMockSynchronizer(undefined);
96+
const persistence = createMockPersistence(undefined);
13797

13898
const { result } = renderHook(() =>
13999
useCollectionVariables({
140100
params: { pageSize: 20 },
141-
synchronizer,
101+
persistence,
142102
}),
143103
);
144104

145105
act(() => {
146106
result.current.control.addFilter("status", "eq", "ACTIVE");
147107
});
148108

149-
expect(synchronizer.write).toHaveBeenCalledWith(
109+
expect(persistence.write).toHaveBeenCalledWith(
150110
expect.objectContaining({
151111
filters: [{ field: "status", operator: "eq", value: "ACTIVE" }],
152112
pageSize: 20,
@@ -155,20 +115,20 @@ describe("useCollectionVariables with synchronizer", () => {
155115
});
156116

157117
it("calls write on sort change", () => {
158-
const synchronizer = createMockSynchronizer(undefined);
118+
const persistence = createMockPersistence(undefined);
159119

160120
const { result } = renderHook(() =>
161121
useCollectionVariables({
162122
params: { pageSize: 20 },
163-
synchronizer,
123+
persistence,
164124
}),
165125
);
166126

167127
act(() => {
168128
result.current.control.setSort("name", "Desc");
169129
});
170130

171-
expect(synchronizer.write).toHaveBeenCalledWith(
131+
expect(persistence.write).toHaveBeenCalledWith(
172132
expect.objectContaining({
173133
sort: [{ field: "name", direction: "Desc" }],
174134
pageSize: 20,
@@ -177,27 +137,27 @@ describe("useCollectionVariables with synchronizer", () => {
177137
});
178138

179139
it("calls write on pageSize change", () => {
180-
const synchronizer = createMockSynchronizer(undefined);
140+
const persistence = createMockPersistence(undefined);
181141

182142
const { result } = renderHook(() =>
183143
useCollectionVariables({
184144
params: { pageSize: 20 },
185-
synchronizer,
145+
persistence,
186146
}),
187147
);
188148

189149
act(() => {
190150
result.current.control.setPageSize(50);
191151
});
192152

193-
expect(synchronizer.write).toHaveBeenCalledWith(
153+
expect(persistence.write).toHaveBeenCalledWith(
194154
expect.objectContaining({
195155
pageSize: 50,
196156
}),
197157
);
198158
});
199159

200-
it("does not crash when no synchronizer is provided", () => {
160+
it("does not crash when no persistence is provided", () => {
201161
const { result } = renderHook(() => useCollectionVariables({ params: { pageSize: 20 } }));
202162

203163
act(() => {

0 commit comments

Comments
 (0)