Skip to content

Commit d70b66d

Browse files
Merge pull request #23 from HichemTab-tech/add-tests-for-shared-states-selector
Add tests for shared states selector
2 parents a4a0342 + a268dd9 commit d70b66d

File tree

2 files changed

+140
-0
lines changed

2 files changed

+140
-0
lines changed

src/SharedValuesManager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,10 @@ export class SharedValuesApi<T extends SharedValue, V, R = T> {
174174
keyStr = ensureNonEmptyString(key);
175175
}
176176
const prefix: Prefix = scope || "_global";
177+
178+
this.sharedData.init(keyStr, prefix, value);
177179
this.sharedData.setValue(keyStr, prefix, value);
180+
this.sharedData.callListeners(keyStr, prefix);
178181
}
179182

180183
/**
@@ -194,6 +197,7 @@ export class SharedValuesApi<T extends SharedValue, V, R = T> {
194197
const [prefix, keyWithoutPrefix] = SharedValuesManager.extractPrefix(key);
195198
if (prefix === prefixToSearch) {
196199
this.sharedData.clear(keyWithoutPrefix, prefix);
200+
this.sharedData.callListeners(keyWithoutPrefix, prefix);
197201
return;
198202
}
199203
});

tests/index.test.tsx

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
sharedSubscriptionsApi,
1212
useSharedFunction,
1313
useSharedState,
14+
useSharedStateSelector,
1415
useSharedSubscription
1516
} from "../src";
1617
import type {Subscriber, SubscriberEvents} from "../src/hooks/use-shared-subscription";
@@ -524,3 +525,138 @@ describe('useSharedSubscription', () => {
524525
expect(clearedState.error).toBeUndefined();
525526
});
526527
});
528+
529+
describe('useSharedStateSelector', () => {
530+
const initialState = {a: 1, b: 2, nested: {c: 'hello'}};
531+
const sharedObjectState = createSharedState(initialState);
532+
533+
it('should select a slice of state and only re-render when that slice changes', () => {
534+
const renderSpyA = vi.fn();
535+
const renderSpyB = vi.fn();
536+
537+
const ComponentA = () => {
538+
const a = useSharedStateSelector(sharedObjectState, state => state.a);
539+
renderSpyA();
540+
return <span data-testid="a-value">{a}</span>;
541+
};
542+
543+
const ComponentB = () => {
544+
const b = useSharedStateSelector(sharedObjectState, state => state.b);
545+
renderSpyB();
546+
return <span data-testid="b-value">{b}</span>;
547+
};
548+
549+
const Controller = () => {
550+
const [state, setState] = useSharedState(sharedObjectState);
551+
return (
552+
<div>
553+
<button onClick={() => setState(s => ({...s, a: s.a + 1}))}>inc a</button>
554+
<button onClick={() => setState(s => ({...s, b: s.b + 1}))}>inc b</button>
555+
<span data-testid="full-state">{JSON.stringify(state)}</span>
556+
</div>
557+
);
558+
};
559+
560+
render(
561+
<>
562+
<ComponentA/>
563+
<ComponentB/>
564+
<Controller/>
565+
</>
566+
);
567+
568+
// Initial render
569+
expect(screen.getByTestId('a-value').textContent).toBe('1');
570+
expect(screen.getByTestId('b-value').textContent).toBe('2');
571+
expect(renderSpyA).toHaveBeenCalledTimes(1);
572+
expect(renderSpyB).toHaveBeenCalledTimes(1);
573+
574+
// Update 'b', only ComponentB should re-render
575+
act(() => {
576+
fireEvent.click(screen.getByText('inc b'));
577+
});
578+
579+
expect(screen.getByTestId('a-value').textContent).toBe('1');
580+
expect(screen.getByTestId('b-value').textContent).toBe('3');
581+
expect(renderSpyA).toHaveBeenCalledTimes(1); // Should not re-render
582+
expect(renderSpyB).toHaveBeenCalledTimes(2); // Should re-render
583+
584+
// Update 'a', only ComponentA should re-render
585+
act(() => {
586+
fireEvent.click(screen.getByText('inc a'));
587+
});
588+
589+
expect(screen.getByTestId('a-value').textContent).toBe('2');
590+
expect(screen.getByTestId('b-value').textContent).toBe('3');
591+
expect(renderSpyA).toHaveBeenCalledTimes(2); // Should re-render
592+
expect(renderSpyB).toHaveBeenCalledTimes(2); // Should not re-render
593+
});
594+
595+
it('should work with string keys', () => {
596+
const renderSpy = vi.fn();
597+
const key = 'string-key-state';
598+
sharedStatesApi.set(key, {val: 100});
599+
600+
const SelectorComponent = () => {
601+
const val = useSharedStateSelector<{ val: number }, typeof key, number>(key, state => state.val);
602+
renderSpy();
603+
return <span data-testid="val">{val}</span>;
604+
};
605+
606+
render(<SelectorComponent/>);
607+
expect(screen.getByTestId('val').textContent).toBe('100');
608+
expect(renderSpy).toHaveBeenCalledTimes(1);
609+
610+
// Update state
611+
act(() => {
612+
sharedStatesApi.set(key, {val: 200});
613+
});
614+
615+
expect(screen.getByTestId('val').textContent).toBe('200');
616+
expect(renderSpy).toHaveBeenCalledTimes(2);
617+
});
618+
619+
it('should perform deep comparison correctly', () => {
620+
const renderSpy = vi.fn();
621+
const nestedState = createSharedState({ a: 1, nested: { c: 'initial' } });
622+
623+
const NestedSelector = () => {
624+
const nested = useSharedStateSelector(nestedState, state => state.nested);
625+
renderSpy();
626+
return <span data-testid="nested-c">{nested.c}</span>;
627+
};
628+
629+
const Controller = () => {
630+
const [, setState] = useSharedState(nestedState);
631+
return (
632+
<div>
633+
<button onClick={() => setState(s => ({ ...s, a: s.a + 1 }))}>update outer</button>
634+
<button onClick={() => setState(s => ({ ...s, nested: { c: 'updated' } }))}>update inner</button>
635+
</div>
636+
);
637+
};
638+
639+
render(
640+
<>
641+
<NestedSelector />
642+
<Controller />
643+
</>
644+
);
645+
646+
expect(screen.getByTestId('nested-c').textContent).toBe('initial');
647+
expect(renderSpy).toHaveBeenCalledTimes(1);
648+
649+
// Update outer property, should not re-render because the selected object is deep-equal
650+
act(() => {
651+
fireEvent.click(screen.getByText('update outer'));
652+
});
653+
expect(renderSpy).toHaveBeenCalledTimes(1);
654+
655+
// Update inner property, should re-render
656+
act(() => {
657+
fireEvent.click(screen.getByText('update inner'));
658+
});
659+
expect(screen.getByTestId('nested-c').textContent).toBe('updated');
660+
expect(renderSpy).toHaveBeenCalledTimes(2);
661+
});
662+
});

0 commit comments

Comments
 (0)