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 CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * react-shared-states v1.0.20
2
+ * react-shared-states v1.0.21
3
3
  * (c) Hichem Taboukouyout
4
4
  * Released under the MIT License.
5
5
  * Github: github.com/HichemTab-tech/react-shared-states
package/dist/main.min.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * react-shared-states v1.0.20
2
+ * react-shared-states v1.0.21
3
3
  * (c) Hichem Taboukouyout
4
4
  * Released under the MIT License.
5
5
  * Github: github.com/HichemTab-tech/react-shared-states
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-shared-states",
3
- "version": "1.0.20",
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}`);
@@ -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
- });