react-shared-states 1.0.20 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.esm.js +1 -1
- package/dist/main.min.js +1 -1
- package/package.json +4 -3
- package/assets/banner.png +0 -0
- package/scripts/bumper.js +0 -24
- package/tests/index.test.tsx +0 -732
- package/vite.config.ts +0 -49
package/dist/main.esm.js
CHANGED
package/dist/main.min.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-shared-states",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Global state made as simple as useState, with zero config, built-in async caching, and automatic scoping.",
|
|
6
6
|
"keywords": [
|
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
],
|
|
15
15
|
"main": "dist/main.min.js",
|
|
16
16
|
"module": "dist/main.esm.js",
|
|
17
|
+
"types": "dist/index.d.ts",
|
|
17
18
|
"exports": {
|
|
18
19
|
".": {
|
|
19
20
|
"import": "./dist/main.esm.js",
|
|
20
|
-
"require": "./dist/main.min.js"
|
|
21
|
+
"require": "./dist/main.min.js",
|
|
22
|
+
"types": "./dist/index.d.ts"
|
|
21
23
|
}
|
|
22
24
|
},
|
|
23
|
-
"types": "dist/index.d.ts",
|
|
24
25
|
"author": "Hichem Taboukouyout <hichem.taboukouyout@hichemtab-tech.me>",
|
|
25
26
|
"repository": {
|
|
26
27
|
"type": "git",
|
package/assets/banner.png
DELETED
|
Binary file
|
package/scripts/bumper.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
|
|
3
|
-
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
|
4
|
-
const version = packageJson.version;
|
|
5
|
-
const [major, minor, patchWord] = version.split('.');
|
|
6
|
-
let newPatch;
|
|
7
|
-
if (patchWord.includes('-')) {
|
|
8
|
-
// Handle pre-release versions by extracting the numeric part
|
|
9
|
-
newPatch = parseInt(patchWord.split('-')[0], 10);
|
|
10
|
-
newPatch += 1;
|
|
11
|
-
// Reconstruct the pre-release version
|
|
12
|
-
newPatch = `${newPatch}-${patchWord.split('-')[1]}`;
|
|
13
|
-
}
|
|
14
|
-
else{
|
|
15
|
-
newPatch = parseInt(patchWord, 10) + 1;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Increment patch version
|
|
19
|
-
packageJson.version = `${major}.${minor}.${newPatch}`;
|
|
20
|
-
|
|
21
|
-
// Write back to package.json with proper formatting
|
|
22
|
-
fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 2) + '\n');
|
|
23
|
-
|
|
24
|
-
console.log(`Version bumped from ${version} to ${packageJson.version}`);
|
package/tests/index.test.tsx
DELETED
|
@@ -1,732 +0,0 @@
|
|
|
1
|
-
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
2
|
-
import React, {useEffect} from 'react'
|
|
3
|
-
import {act, cleanup, fireEvent, render, screen} from '@testing-library/react'
|
|
4
|
-
import {
|
|
5
|
-
createSharedFunction,
|
|
6
|
-
createSharedState,
|
|
7
|
-
createSharedSubscription,
|
|
8
|
-
sharedFunctionsApi,
|
|
9
|
-
sharedStatesApi,
|
|
10
|
-
SharedStatesProvider,
|
|
11
|
-
sharedSubscriptionsApi,
|
|
12
|
-
useSharedFunction,
|
|
13
|
-
useSharedState,
|
|
14
|
-
useSharedStateSelector,
|
|
15
|
-
useSharedSubscription
|
|
16
|
-
} from "../src";
|
|
17
|
-
import type {Subscriber, SubscriberEvents} from "../src/hooks/use-shared-subscription";
|
|
18
|
-
|
|
19
|
-
// Mocking random to have predictable keys for created states/functions/subscriptions
|
|
20
|
-
vi.mock('../src/lib/utils', async (importActual) => {
|
|
21
|
-
const actual = await importActual<typeof import('../src/lib/utils')>();
|
|
22
|
-
let count = 0;
|
|
23
|
-
// noinspection JSUnusedGlobalSymbols
|
|
24
|
-
return {
|
|
25
|
-
...actual,
|
|
26
|
-
random: () => `test-key-${count++}`,
|
|
27
|
-
};
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
cleanup();
|
|
32
|
-
// Reset the mocked random key counter
|
|
33
|
-
vi.clearAllMocks();
|
|
34
|
-
});
|
|
35
|
-
afterEach(() => {
|
|
36
|
-
vi.useRealTimers();
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
describe('useSharedState', () => {
|
|
40
|
-
it('should share state between two components', () => {
|
|
41
|
-
const TestComponent1 = () => {
|
|
42
|
-
const [count] = useSharedState('count', 0);
|
|
43
|
-
return <span data-testid="value1">{count}</span>;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const TestComponent2 = () => {
|
|
47
|
-
const [count, setCount] = useSharedState('count', 0);
|
|
48
|
-
return (
|
|
49
|
-
<div>
|
|
50
|
-
<span data-testid="value2">{count}</span>
|
|
51
|
-
<button onClick={() => setCount(c => c + 1)}>inc</button>
|
|
52
|
-
</div>
|
|
53
|
-
);
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
render(
|
|
57
|
-
<>
|
|
58
|
-
<TestComponent1/>
|
|
59
|
-
<TestComponent2/>
|
|
60
|
-
</>
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
expect(screen.getByTestId('value1').textContent).toBe('0');
|
|
64
|
-
expect(screen.getByTestId('value2').textContent).toBe('0');
|
|
65
|
-
|
|
66
|
-
act(() => {
|
|
67
|
-
fireEvent.click(screen.getByText('inc'));
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
expect(screen.getByTestId('value1').textContent).toBe('1');
|
|
71
|
-
expect(screen.getByTestId('value2').textContent).toBe('1');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should isolate state with SharedStatesProvider', () => {
|
|
75
|
-
const TestComponent = () => {
|
|
76
|
-
const [count, setCount] = useSharedState('count', 0);
|
|
77
|
-
return (
|
|
78
|
-
<div>
|
|
79
|
-
<span>{count}</span>
|
|
80
|
-
<button onClick={() => setCount(c => c + 1)}>inc</button>
|
|
81
|
-
</div>
|
|
82
|
-
);
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
render(
|
|
86
|
-
<div>
|
|
87
|
-
<div data-testid="scope1">
|
|
88
|
-
<SharedStatesProvider scopeName="scope1">
|
|
89
|
-
<TestComponent/>
|
|
90
|
-
</SharedStatesProvider>
|
|
91
|
-
</div>
|
|
92
|
-
<div data-testid="scope2">
|
|
93
|
-
<SharedStatesProvider scopeName="scope2">
|
|
94
|
-
<TestComponent/>
|
|
95
|
-
</SharedStatesProvider>
|
|
96
|
-
</div>
|
|
97
|
-
</div>
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
const scope1Button = screen.getAllByText('inc')[0];
|
|
101
|
-
const scope2Button = screen.getAllByText('inc')[1];
|
|
102
|
-
|
|
103
|
-
act(() => {
|
|
104
|
-
fireEvent.click(scope1Button);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
expect(screen.getByTestId('scope1').textContent).toContain('1');
|
|
108
|
-
expect(screen.getByTestId('scope2').textContent).toContain('0');
|
|
109
|
-
|
|
110
|
-
act(() => {
|
|
111
|
-
fireEvent.click(scope2Button);
|
|
112
|
-
fireEvent.click(scope2Button);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
expect(screen.getByTestId('scope1').textContent).toContain('1');
|
|
116
|
-
expect(screen.getByTestId('scope2').textContent).toContain('2');
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should work with createSharedState', () => {
|
|
120
|
-
const sharedCounter = createSharedState(10);
|
|
121
|
-
|
|
122
|
-
const TestComponent1 = () => {
|
|
123
|
-
const [count] = useSharedState(sharedCounter);
|
|
124
|
-
return <span data-testid="value1">{count}</span>;
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const TestComponent2 = () => {
|
|
128
|
-
const [count, setCount] = useSharedState(sharedCounter);
|
|
129
|
-
return <button onClick={() => setCount(count + 5)}>inc</button>;
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
render(
|
|
133
|
-
<>
|
|
134
|
-
<TestComponent1/>
|
|
135
|
-
<TestComponent2/>
|
|
136
|
-
</>
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
expect(screen.getByTestId('value1').textContent).toBe('10');
|
|
140
|
-
|
|
141
|
-
act(() => {
|
|
142
|
-
fireEvent.click(screen.getByText('inc'));
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
expect(screen.getByTestId('value1').textContent).toBe('15');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('should allow direct api manipulation with createSharedState objects', () => {
|
|
149
|
-
const sharedCounter = createSharedState(100);
|
|
150
|
-
|
|
151
|
-
// Get initial value
|
|
152
|
-
expect(sharedStatesApi.get(sharedCounter)).toBe(100);
|
|
153
|
-
|
|
154
|
-
// Set a new value
|
|
155
|
-
act(() => {
|
|
156
|
-
sharedStatesApi.set(sharedCounter, 200);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// Get updated value
|
|
160
|
-
expect(sharedStatesApi.get(sharedCounter)).toBe(200);
|
|
161
|
-
|
|
162
|
-
// Update the value
|
|
163
|
-
act(() => {
|
|
164
|
-
sharedStatesApi.update(sharedCounter, (prev) => prev + 50);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// Get updated value after update
|
|
168
|
-
expect(sharedStatesApi.get(sharedCounter)).toBe(250);
|
|
169
|
-
|
|
170
|
-
// Clear the value
|
|
171
|
-
act(() => {
|
|
172
|
-
sharedStatesApi.clear(sharedCounter);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// Get value after clear (should be initial value because createSharedState re-initializes it)
|
|
176
|
-
expect(sharedStatesApi.get(sharedCounter)).toBe(100);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('should be able to subscribe to state changes from api', () => {
|
|
180
|
-
const sharedCounter = createSharedState(100);
|
|
181
|
-
|
|
182
|
-
const subscribeCallback = vi.fn();
|
|
183
|
-
|
|
184
|
-
act(() => {
|
|
185
|
-
sharedStatesApi.subscribe(sharedCounter, () => {
|
|
186
|
-
subscribeCallback();
|
|
187
|
-
expect(sharedStatesApi.get(sharedCounter)).toBe(200);
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
// Update the value
|
|
192
|
-
act(() => {
|
|
193
|
-
sharedStatesApi.set(sharedCounter,200);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
expect(subscribeCallback).toHaveBeenCalledTimes(1);
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
describe('useSharedFunction', () => {
|
|
201
|
-
const mockApiCall = vi.fn((...args: any[]) => new Promise(resolve => setTimeout(() => resolve(`result: ${args.join(',')}`), 100)));
|
|
202
|
-
|
|
203
|
-
beforeEach(() => {
|
|
204
|
-
mockApiCall.mockClear();
|
|
205
|
-
vi.useFakeTimers();
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
const TestComponent = ({fnKey, sharedFn}: { fnKey: string, sharedFn?: any }) => {
|
|
209
|
-
const {state, trigger, forceTrigger, clear} = sharedFn ? useSharedFunction(sharedFn) : useSharedFunction(fnKey, mockApiCall);
|
|
210
|
-
return (
|
|
211
|
-
<div>
|
|
212
|
-
{state.isLoading && <span>Loading...</span>}
|
|
213
|
-
{state.error as any && <span>{String(state.error)}</span>}
|
|
214
|
-
{state.results && <span data-testid="result">{String(state.results)}</span>}
|
|
215
|
-
<button onClick={() => trigger('arg1')}>trigger</button>
|
|
216
|
-
<button onClick={() => forceTrigger('arg2')}>force</button>
|
|
217
|
-
<button onClick={() => clear()}>clear</button>
|
|
218
|
-
</div>
|
|
219
|
-
);
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
it('should handle async function lifecycle', async () => {
|
|
223
|
-
render(<TestComponent fnKey="test-fn"/>);
|
|
224
|
-
|
|
225
|
-
// Initial state
|
|
226
|
-
expect(screen.queryByText('Loading...')).toBeNull();
|
|
227
|
-
expect(screen.queryByTestId('result')).toBeNull();
|
|
228
|
-
|
|
229
|
-
// Trigger
|
|
230
|
-
act(() => {
|
|
231
|
-
fireEvent.click(screen.getByText('trigger'));
|
|
232
|
-
});
|
|
233
|
-
expect(screen.getByText('Loading...')).toBeDefined();
|
|
234
|
-
|
|
235
|
-
// Resolve
|
|
236
|
-
await act(async () => {
|
|
237
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
238
|
-
});
|
|
239
|
-
expect(screen.queryByText('Loading...')).toBeNull();
|
|
240
|
-
expect(screen.getByTestId('result').textContent).toBe('result: arg1');
|
|
241
|
-
expect(mockApiCall).toHaveBeenCalledTimes(1);
|
|
242
|
-
expect(mockApiCall).toHaveBeenCalledWith('arg1');
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it('should not trigger if already running or has data', async () => {
|
|
246
|
-
render(<TestComponent fnKey="test-fn"/>);
|
|
247
|
-
act(() => {
|
|
248
|
-
fireEvent.click(screen.getByText('trigger'));
|
|
249
|
-
});
|
|
250
|
-
await act(async () => {
|
|
251
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
252
|
-
});
|
|
253
|
-
expect(mockApiCall).toHaveBeenCalledTimes(1);
|
|
254
|
-
|
|
255
|
-
// Trigger again, should not call mockApiCall
|
|
256
|
-
act(() => {
|
|
257
|
-
fireEvent.click(screen.getByText('trigger'));
|
|
258
|
-
});
|
|
259
|
-
expect(mockApiCall).toHaveBeenCalledTimes(1);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it('should force trigger', async () => {
|
|
263
|
-
render(<TestComponent fnKey="test-fn"/>);
|
|
264
|
-
act(() => {
|
|
265
|
-
fireEvent.click(screen.getByText('trigger'));
|
|
266
|
-
});
|
|
267
|
-
await act(async () => {
|
|
268
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
269
|
-
});
|
|
270
|
-
expect(mockApiCall).toHaveBeenCalledTimes(1);
|
|
271
|
-
|
|
272
|
-
// Force trigger
|
|
273
|
-
act(() => {
|
|
274
|
-
fireEvent.click(screen.getByText('force'));
|
|
275
|
-
});
|
|
276
|
-
expect(screen.getByText('Loading...')).toBeDefined();
|
|
277
|
-
await act(async () => {
|
|
278
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
279
|
-
});
|
|
280
|
-
expect(mockApiCall).toHaveBeenCalledTimes(2);
|
|
281
|
-
expect(mockApiCall).toHaveBeenCalledWith('arg2');
|
|
282
|
-
expect(screen.getByTestId('result').textContent).toBe('result: arg2');
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it('should clear state', async () => {
|
|
286
|
-
render(<TestComponent fnKey="test-fn"/>);
|
|
287
|
-
act(() => {
|
|
288
|
-
fireEvent.click(screen.getByText('trigger'));
|
|
289
|
-
});
|
|
290
|
-
await act(async () => {
|
|
291
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
292
|
-
});
|
|
293
|
-
expect(screen.getByTestId('result')).toBeDefined();
|
|
294
|
-
|
|
295
|
-
act(() => {
|
|
296
|
-
fireEvent.click(screen.getByText('clear'));
|
|
297
|
-
});
|
|
298
|
-
expect(screen.queryByTestId('result')).toBeNull();
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it('should work with createSharedFunction', async () => {
|
|
302
|
-
const sharedFunction = createSharedFunction(mockApiCall);
|
|
303
|
-
render(<TestComponent fnKey="unused" sharedFn={sharedFunction}/>);
|
|
304
|
-
|
|
305
|
-
act(() => {
|
|
306
|
-
fireEvent.click(screen.getByText('trigger'));
|
|
307
|
-
});
|
|
308
|
-
await act(async () => {
|
|
309
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
310
|
-
});
|
|
311
|
-
expect(mockApiCall).toHaveBeenCalledTimes(1);
|
|
312
|
-
expect(screen.getByTestId('result').textContent).toBe('result: arg1');
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it('should allow direct api manipulation with createSharedFunction objects', () => {
|
|
316
|
-
const sharedFunction = createSharedFunction(async (arg: string) => `result: ${arg}`);
|
|
317
|
-
|
|
318
|
-
// Get initial state
|
|
319
|
-
const initialState = sharedFunctionsApi.get(sharedFunction);
|
|
320
|
-
expect(initialState.results).toBeUndefined();
|
|
321
|
-
expect(initialState.isLoading).toBe(false);
|
|
322
|
-
expect(initialState.error).toBeUndefined();
|
|
323
|
-
|
|
324
|
-
// Set a new state
|
|
325
|
-
act(() => {
|
|
326
|
-
sharedFunctionsApi.set(sharedFunction, {
|
|
327
|
-
fnState: {
|
|
328
|
-
results: 'test data',
|
|
329
|
-
isLoading: true,
|
|
330
|
-
error: 'test error',
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
// Get updated state
|
|
336
|
-
const updatedState = sharedFunctionsApi.get(sharedFunction);
|
|
337
|
-
expect(updatedState.results).toBe('test data');
|
|
338
|
-
expect(updatedState.isLoading).toBe(true);
|
|
339
|
-
expect(updatedState.error).toBe('test error');
|
|
340
|
-
|
|
341
|
-
// Update the state
|
|
342
|
-
act(() => {
|
|
343
|
-
sharedFunctionsApi.update(sharedFunction, (prev) => ({
|
|
344
|
-
fnState: {
|
|
345
|
-
...prev,
|
|
346
|
-
results: 'updated data',
|
|
347
|
-
}
|
|
348
|
-
}));
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
// Get updated state after update
|
|
352
|
-
const updatedState2 = sharedFunctionsApi.get(sharedFunction);
|
|
353
|
-
expect(updatedState2.results).toBe('updated data');
|
|
354
|
-
|
|
355
|
-
// Clear the value
|
|
356
|
-
act(() => {
|
|
357
|
-
sharedFunctionsApi.clear(sharedFunction);
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
// Get value after clear (should be initial value)
|
|
361
|
-
const clearedState = sharedFunctionsApi.get(sharedFunction);
|
|
362
|
-
expect(clearedState.results).toBeUndefined();
|
|
363
|
-
expect(clearedState.isLoading).toBe(false);
|
|
364
|
-
expect(clearedState.error).toBeUndefined();
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
describe('useSharedSubscription', () => {
|
|
369
|
-
it('should handle subscription lifecycle', () => {
|
|
370
|
-
const mockSubscriber = vi.fn<Subscriber<string>>((set) => {
|
|
371
|
-
set('initial data');
|
|
372
|
-
return () => {
|
|
373
|
-
};
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
const TestComponent = () => {
|
|
377
|
-
const {state: {data}, trigger} = useSharedSubscription('test-sub', mockSubscriber);
|
|
378
|
-
|
|
379
|
-
useEffect(() => {
|
|
380
|
-
trigger();
|
|
381
|
-
}, []);
|
|
382
|
-
|
|
383
|
-
return <span data-testid="data">{data}</span>;
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
render(<TestComponent/>);
|
|
387
|
-
|
|
388
|
-
expect(mockSubscriber).toHaveBeenCalledTimes(1);
|
|
389
|
-
expect(screen.getByTestId('data').textContent).toBe('initial data');
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
it('should allow direct api manipulation with createSharedSubscription objects', () => {
|
|
393
|
-
const mockSubscriber = vi.fn();
|
|
394
|
-
const sharedSubscription = createSharedSubscription(mockSubscriber);
|
|
395
|
-
|
|
396
|
-
// Get initial state
|
|
397
|
-
const initialState = sharedSubscriptionsApi.get(sharedSubscription);
|
|
398
|
-
expect(initialState.data).toBeUndefined();
|
|
399
|
-
expect(initialState.isLoading).toBe(false);
|
|
400
|
-
expect(initialState.error).toBeUndefined();
|
|
401
|
-
|
|
402
|
-
// Set a new state
|
|
403
|
-
act(() => {
|
|
404
|
-
sharedSubscriptionsApi.set(sharedSubscription, {
|
|
405
|
-
fnState: {
|
|
406
|
-
data: 'test data',
|
|
407
|
-
isLoading: true,
|
|
408
|
-
error: 'test error',
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
// Get updated state
|
|
414
|
-
const updatedState = sharedSubscriptionsApi.get(sharedSubscription);
|
|
415
|
-
expect(updatedState.data).toBe('test data');
|
|
416
|
-
expect(updatedState.isLoading).toBe(true);
|
|
417
|
-
expect(updatedState.error).toBe('test error');
|
|
418
|
-
|
|
419
|
-
// Update the state
|
|
420
|
-
act(() => {
|
|
421
|
-
sharedSubscriptionsApi.update(sharedSubscription, (prev) => ({
|
|
422
|
-
fnState: {
|
|
423
|
-
...prev,
|
|
424
|
-
data: 'updated data',
|
|
425
|
-
}
|
|
426
|
-
}));
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
// Get updated state after update
|
|
430
|
-
const updatedState2 = sharedSubscriptionsApi.get(sharedSubscription);
|
|
431
|
-
expect(updatedState2.data).toBe('updated data');
|
|
432
|
-
|
|
433
|
-
// Clear the value
|
|
434
|
-
act(() => {
|
|
435
|
-
sharedSubscriptionsApi.clear(sharedSubscription);
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
// Get value after clear (should be initial value)
|
|
439
|
-
const clearedState = sharedSubscriptionsApi.get(sharedSubscription);
|
|
440
|
-
expect(clearedState.data).toBeUndefined();
|
|
441
|
-
expect(clearedState.isLoading).toBe(false);
|
|
442
|
-
expect(clearedState.error).toBeUndefined();
|
|
443
|
-
});
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
describe('useSharedSubscription', () => {
|
|
447
|
-
let mockSubscriber: (set: SubscriberEvents.Set<any>, onError: SubscriberEvents.OnError, onCompletion: SubscriberEvents.OnCompletion) => () => void;
|
|
448
|
-
const mockUnsubscribe = vi.fn();
|
|
449
|
-
|
|
450
|
-
beforeEach(() => {
|
|
451
|
-
mockUnsubscribe.mockClear();
|
|
452
|
-
mockSubscriber = vi.fn((set, _onError, onCompletion) => {
|
|
453
|
-
// Simulate async subscription
|
|
454
|
-
const timeout = setTimeout(() => {
|
|
455
|
-
set('initial data');
|
|
456
|
-
onCompletion();
|
|
457
|
-
}, 100);
|
|
458
|
-
return () => {
|
|
459
|
-
clearTimeout(timeout);
|
|
460
|
-
mockUnsubscribe();
|
|
461
|
-
};
|
|
462
|
-
});
|
|
463
|
-
vi.useFakeTimers();
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
const TestComponent = ({subKey, sharedSub}: { subKey: string, sharedSub?: any }) => {
|
|
467
|
-
const {state, trigger, unsubscribe} = sharedSub ? useSharedSubscription(sharedSub) : useSharedSubscription(subKey, mockSubscriber);
|
|
468
|
-
return (
|
|
469
|
-
<div>
|
|
470
|
-
{state.isLoading && <span>Loading...</span>}
|
|
471
|
-
{state.error && <span>{String(state.error)}</span>}
|
|
472
|
-
{state.data && <span data-testid="data">{String(state.data)}</span>}
|
|
473
|
-
<span>Subscribed: {String(state.subscribed)}</span>
|
|
474
|
-
<button onClick={() => trigger()}>subscribe</button>
|
|
475
|
-
<button onClick={() => unsubscribe()}>unsubscribe</button>
|
|
476
|
-
</div>
|
|
477
|
-
);
|
|
478
|
-
};
|
|
479
|
-
|
|
480
|
-
it('should handle subscription lifecycle', async () => {
|
|
481
|
-
render(<TestComponent subKey="test-sub"/>);
|
|
482
|
-
|
|
483
|
-
// Initial state
|
|
484
|
-
expect(screen.getByText('Subscribed: false')).toBeDefined();
|
|
485
|
-
|
|
486
|
-
// Trigger subscription
|
|
487
|
-
act(() => {
|
|
488
|
-
fireEvent.click(screen.getByText('subscribe'));
|
|
489
|
-
});
|
|
490
|
-
expect(screen.getByText('Loading...')).toBeDefined();
|
|
491
|
-
|
|
492
|
-
// Subscription completes
|
|
493
|
-
await act(async () => {
|
|
494
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
495
|
-
});
|
|
496
|
-
expect(screen.queryByText('Loading...')).toBeNull();
|
|
497
|
-
expect(screen.getByTestId('data').textContent).toBe('initial data');
|
|
498
|
-
expect(screen.getByText('Subscribed: true')).toBeDefined();
|
|
499
|
-
expect(mockSubscriber).toHaveBeenCalledTimes(1);
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
it('should unsubscribe', async () => {
|
|
503
|
-
render(<TestComponent subKey="test-sub"/>);
|
|
504
|
-
act(() => {
|
|
505
|
-
fireEvent.click(screen.getByText('subscribe'));
|
|
506
|
-
});
|
|
507
|
-
await act(async () => {
|
|
508
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
act(() => {
|
|
512
|
-
fireEvent.click(screen.getByText('unsubscribe'));
|
|
513
|
-
});
|
|
514
|
-
expect(mockUnsubscribe).toHaveBeenCalledTimes(1);
|
|
515
|
-
expect(screen.getByText('Subscribed: false')).toBeDefined();
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
it('should automatically unsubscribe on unmount', async () => {
|
|
519
|
-
const {unmount} = render(<TestComponent subKey="test-sub"/>);
|
|
520
|
-
act(() => {
|
|
521
|
-
fireEvent.click(screen.getByText('subscribe'));
|
|
522
|
-
});
|
|
523
|
-
await act(async () => {
|
|
524
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
unmount();
|
|
528
|
-
expect(mockUnsubscribe).toHaveBeenCalledTimes(1);
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
it('should work with createSharedSubscription', async () => {
|
|
532
|
-
const sharedSubscription = createSharedSubscription(mockSubscriber);
|
|
533
|
-
render(<TestComponent subKey="unused" sharedSub={sharedSubscription}/>);
|
|
534
|
-
|
|
535
|
-
act(() => {
|
|
536
|
-
fireEvent.click(screen.getByText('subscribe'));
|
|
537
|
-
});
|
|
538
|
-
await act(async () => {
|
|
539
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
540
|
-
});
|
|
541
|
-
expect(mockSubscriber).toHaveBeenCalledTimes(1);
|
|
542
|
-
expect(screen.getByTestId('data').textContent).toBe('initial data');
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
it('should allow direct api manipulation with createSharedSubscription objects', () => {
|
|
546
|
-
const mockSubscriber = vi.fn();
|
|
547
|
-
const sharedSubscription = createSharedSubscription(mockSubscriber);
|
|
548
|
-
|
|
549
|
-
// Get initial state
|
|
550
|
-
const initialState = sharedSubscriptionsApi.get(sharedSubscription);
|
|
551
|
-
expect(initialState.data).toBeUndefined();
|
|
552
|
-
expect(initialState.isLoading).toBe(false);
|
|
553
|
-
expect(initialState.error).toBeUndefined();
|
|
554
|
-
|
|
555
|
-
// Set a new state
|
|
556
|
-
act(() => {
|
|
557
|
-
sharedSubscriptionsApi.set(sharedSubscription, {
|
|
558
|
-
fnState: {
|
|
559
|
-
data: 'test data',
|
|
560
|
-
isLoading: true,
|
|
561
|
-
error: 'test error',
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
// Get updated state
|
|
567
|
-
const updatedState = sharedSubscriptionsApi.get(sharedSubscription);
|
|
568
|
-
expect(updatedState.data).toBe('test data');
|
|
569
|
-
expect(updatedState.isLoading).toBe(true);
|
|
570
|
-
expect(updatedState.error).toBe('test error');
|
|
571
|
-
|
|
572
|
-
// Update the state
|
|
573
|
-
act(() => {
|
|
574
|
-
sharedSubscriptionsApi.update(sharedSubscription, (prev) => ({
|
|
575
|
-
fnState: {
|
|
576
|
-
...prev,
|
|
577
|
-
data: 'updated data',
|
|
578
|
-
}
|
|
579
|
-
}));
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
// Get updated state after update
|
|
583
|
-
const updatedState2 = sharedSubscriptionsApi.get(sharedSubscription);
|
|
584
|
-
expect(updatedState2.data).toBe('updated data');
|
|
585
|
-
|
|
586
|
-
// Clear the value
|
|
587
|
-
act(() => {
|
|
588
|
-
sharedSubscriptionsApi.clear(sharedSubscription);
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
// Get value after clear (should be initial value)
|
|
592
|
-
const clearedState = sharedSubscriptionsApi.get(sharedSubscription);
|
|
593
|
-
expect(clearedState.data).toBeUndefined();
|
|
594
|
-
expect(clearedState.isLoading).toBe(false);
|
|
595
|
-
expect(clearedState.error).toBeUndefined();
|
|
596
|
-
});
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
describe('useSharedStateSelector', () => {
|
|
600
|
-
const initialState = {a: 1, b: 2, nested: {c: 'hello'}};
|
|
601
|
-
const sharedObjectState = createSharedState(initialState);
|
|
602
|
-
|
|
603
|
-
it('should select a slice of state and only re-render when that slice changes', () => {
|
|
604
|
-
const renderSpyA = vi.fn();
|
|
605
|
-
const renderSpyB = vi.fn();
|
|
606
|
-
|
|
607
|
-
const ComponentA = () => {
|
|
608
|
-
const a = useSharedStateSelector(sharedObjectState, state => state.a);
|
|
609
|
-
renderSpyA();
|
|
610
|
-
return <span data-testid="a-value">{a}</span>;
|
|
611
|
-
};
|
|
612
|
-
|
|
613
|
-
const ComponentB = () => {
|
|
614
|
-
const b = useSharedStateSelector(sharedObjectState, state => state.b);
|
|
615
|
-
renderSpyB();
|
|
616
|
-
return <span data-testid="b-value">{b}</span>;
|
|
617
|
-
};
|
|
618
|
-
|
|
619
|
-
const Controller = () => {
|
|
620
|
-
const [state, setState] = useSharedState(sharedObjectState);
|
|
621
|
-
return (
|
|
622
|
-
<div>
|
|
623
|
-
<button onClick={() => setState(s => ({...s, a: s.a + 1}))}>inc a</button>
|
|
624
|
-
<button onClick={() => setState(s => ({...s, b: s.b + 1}))}>inc b</button>
|
|
625
|
-
<span data-testid="full-state">{JSON.stringify(state)}</span>
|
|
626
|
-
</div>
|
|
627
|
-
);
|
|
628
|
-
};
|
|
629
|
-
|
|
630
|
-
render(
|
|
631
|
-
<>
|
|
632
|
-
<ComponentA/>
|
|
633
|
-
<ComponentB/>
|
|
634
|
-
<Controller/>
|
|
635
|
-
</>
|
|
636
|
-
);
|
|
637
|
-
|
|
638
|
-
// Initial render
|
|
639
|
-
expect(screen.getByTestId('a-value').textContent).toBe('1');
|
|
640
|
-
expect(screen.getByTestId('b-value').textContent).toBe('2');
|
|
641
|
-
expect(renderSpyA).toHaveBeenCalledTimes(1);
|
|
642
|
-
expect(renderSpyB).toHaveBeenCalledTimes(1);
|
|
643
|
-
|
|
644
|
-
// Update 'b', only ComponentB should re-render
|
|
645
|
-
act(() => {
|
|
646
|
-
fireEvent.click(screen.getByText('inc b'));
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
expect(screen.getByTestId('a-value').textContent).toBe('1');
|
|
650
|
-
expect(screen.getByTestId('b-value').textContent).toBe('3');
|
|
651
|
-
expect(renderSpyA).toHaveBeenCalledTimes(1); // Should not re-render
|
|
652
|
-
expect(renderSpyB).toHaveBeenCalledTimes(2); // Should re-render
|
|
653
|
-
|
|
654
|
-
// Update 'a', only ComponentA should re-render
|
|
655
|
-
act(() => {
|
|
656
|
-
fireEvent.click(screen.getByText('inc a'));
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
expect(screen.getByTestId('a-value').textContent).toBe('2');
|
|
660
|
-
expect(screen.getByTestId('b-value').textContent).toBe('3');
|
|
661
|
-
expect(renderSpyA).toHaveBeenCalledTimes(2); // Should re-render
|
|
662
|
-
expect(renderSpyB).toHaveBeenCalledTimes(2); // Should not re-render
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
it('should work with string keys', () => {
|
|
666
|
-
const renderSpy = vi.fn();
|
|
667
|
-
const key = 'string-key-state';
|
|
668
|
-
sharedStatesApi.set(key, {val: 100});
|
|
669
|
-
|
|
670
|
-
const SelectorComponent = () => {
|
|
671
|
-
const val = useSharedStateSelector<{ val: number }, typeof key, number>(key, state => state.val);
|
|
672
|
-
renderSpy();
|
|
673
|
-
return <span data-testid="val">{val}</span>;
|
|
674
|
-
};
|
|
675
|
-
|
|
676
|
-
render(<SelectorComponent/>);
|
|
677
|
-
expect(screen.getByTestId('val').textContent).toBe('100');
|
|
678
|
-
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
679
|
-
|
|
680
|
-
// Update state
|
|
681
|
-
act(() => {
|
|
682
|
-
sharedStatesApi.set(key, {val: 200});
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
expect(screen.getByTestId('val').textContent).toBe('200');
|
|
686
|
-
expect(renderSpy).toHaveBeenCalledTimes(2);
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
it('should perform deep comparison correctly', () => {
|
|
690
|
-
const renderSpy = vi.fn();
|
|
691
|
-
const nestedState = createSharedState({ a: 1, nested: { c: 'initial' } });
|
|
692
|
-
|
|
693
|
-
const NestedSelector = () => {
|
|
694
|
-
const nested = useSharedStateSelector(nestedState, state => state.nested);
|
|
695
|
-
renderSpy();
|
|
696
|
-
return <span data-testid="nested-c">{nested.c}</span>;
|
|
697
|
-
};
|
|
698
|
-
|
|
699
|
-
const Controller = () => {
|
|
700
|
-
const [, setState] = useSharedState(nestedState);
|
|
701
|
-
return (
|
|
702
|
-
<div>
|
|
703
|
-
<button onClick={() => setState(s => ({ ...s, a: s.a + 1 }))}>update outer</button>
|
|
704
|
-
<button onClick={() => setState(s => ({ ...s, nested: { c: 'updated' } }))}>update inner</button>
|
|
705
|
-
</div>
|
|
706
|
-
);
|
|
707
|
-
};
|
|
708
|
-
|
|
709
|
-
render(
|
|
710
|
-
<>
|
|
711
|
-
<NestedSelector />
|
|
712
|
-
<Controller />
|
|
713
|
-
</>
|
|
714
|
-
);
|
|
715
|
-
|
|
716
|
-
expect(screen.getByTestId('nested-c').textContent).toBe('initial');
|
|
717
|
-
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
718
|
-
|
|
719
|
-
// Update outer property, should not re-render because the selected object is deep-equal
|
|
720
|
-
act(() => {
|
|
721
|
-
fireEvent.click(screen.getByText('update outer'));
|
|
722
|
-
});
|
|
723
|
-
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
724
|
-
|
|
725
|
-
// Update inner property, should re-render
|
|
726
|
-
act(() => {
|
|
727
|
-
fireEvent.click(screen.getByText('update inner'));
|
|
728
|
-
});
|
|
729
|
-
expect(screen.getByTestId('nested-c').textContent).toBe('updated');
|
|
730
|
-
expect(renderSpy).toHaveBeenCalledTimes(2);
|
|
731
|
-
});
|
|
732
|
-
});
|
package/vite.config.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'vite';
|
|
2
|
-
import banner from 'vite-plugin-banner';
|
|
3
|
-
import react from '@vitejs/plugin-react';
|
|
4
|
-
import { resolve } from 'path';
|
|
5
|
-
import dts from 'vite-plugin-dts';
|
|
6
|
-
|
|
7
|
-
const version = require('./package.json').version;
|
|
8
|
-
const bannerContent = `/*!
|
|
9
|
-
* react-shared-states v${version}
|
|
10
|
-
* (c) Hichem Taboukouyout
|
|
11
|
-
* Released under the MIT License.
|
|
12
|
-
* Github: github.com/HichemTab-tech/react-shared-states
|
|
13
|
-
*/
|
|
14
|
-
`;
|
|
15
|
-
|
|
16
|
-
export default defineConfig({
|
|
17
|
-
build: {
|
|
18
|
-
lib: {
|
|
19
|
-
entry: resolve(__dirname, 'src/index.ts'), // Library entry point
|
|
20
|
-
name: 'ReactSharedStates',
|
|
21
|
-
fileName: (format: string) => `main${format === 'es' ? '.esm' : '.min'}.js`,
|
|
22
|
-
formats: ['es', 'umd']
|
|
23
|
-
},
|
|
24
|
-
rollupOptions: {
|
|
25
|
-
external: ['react', 'react-dom', 'react/jsx-runtime'], // Mark React, ReactDOM as external
|
|
26
|
-
output: {
|
|
27
|
-
globals: {
|
|
28
|
-
react: 'React',
|
|
29
|
-
'react-dom': 'ReactDOM',
|
|
30
|
-
'react/jsx-runtime': 'jsxRuntime'
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
plugins: [
|
|
36
|
-
react(),
|
|
37
|
-
banner(bannerContent.replace("{{VERSION}}", "")),
|
|
38
|
-
dts({
|
|
39
|
-
entryRoot: 'src', // Base folder for type generation
|
|
40
|
-
outDir: 'dist', // Ensures types go into `dist/`
|
|
41
|
-
insertTypesEntry: true, // Adds the `types` field in package.json
|
|
42
|
-
exclude: ['node_modules', 'dist'], // Exclude unnecessary files
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
],
|
|
46
|
-
define: {
|
|
47
|
-
__REACT_SHARED_STATES_DEV__: process.env.NODE_ENV === 'development' ? 'true' : 'false',
|
|
48
|
-
}
|
|
49
|
-
});
|