Skip to content

Commit 1f0cb01

Browse files
Cross store actions
1 parent 17183b6 commit 1f0cb01

File tree

13 files changed

+327
-209
lines changed

13 files changed

+327
-209
lines changed

examples/advanced-shared/components/color.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,20 @@ const initialState: State = {
1313
color: 'white',
1414
};
1515

16-
const actions = {
16+
export const actions = {
1717
set:
1818
(color: string) =>
1919
({ setState }: StoreActionApi<State>) => {
2020
setState({ color });
2121
},
22+
reset:
23+
() =>
24+
({ setState }: StoreActionApi<State>) => {
25+
setState(initialState);
26+
},
2227
};
2328

24-
const Store = createStore({
29+
export const Store = createStore({
2530
initialState,
2631
actions,
2732
containedBy: ThemingContainer,

examples/advanced-shared/components/width.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
type StoreActionApi,
55
} from 'react-sweet-state';
66
import { ThemingContainer } from './theming';
7+
import { Store as ColorStore, actions as colorActions } from './color';
78

89
type State = {
910
width: number;
@@ -19,6 +20,18 @@ const actions = {
1920
({ setState }: StoreActionApi<State>) => {
2021
setState({ width });
2122
},
23+
reset:
24+
() =>
25+
({ setState }: StoreActionApi<State>) => {
26+
setState(initialState);
27+
},
28+
29+
resetAll:
30+
() =>
31+
({ dispatch, dispatchTo }: StoreActionApi<State>) => {
32+
dispatch(actions.reset());
33+
dispatchTo(ColorStore, colorActions.reset());
34+
},
2235
};
2336

2437
const Store = createStore({

examples/advanced-shared/index.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,21 @@ import ReactDOM from 'react-dom/client';
44
import { useColor } from './components/color';
55
import { useWidth } from './components/width';
66
import { ThemingContainer } from './components/theming';
7+
import { defaults } from 'react-sweet-state';
78

8-
const colors = ['white', 'aliceblue', 'beige', 'gainsboro', 'honeydew'];
9-
const widths = [200, 220, 240, 260, 280];
9+
defaults.devtools = true;
10+
11+
const colors = ['aliceblue', 'beige', 'gainsboro', 'honeydew'];
12+
const widths = [220, 240, 260, 280];
1013
const rand = () => Math.floor(Math.random() * colors.length);
11-
const initialData = { color: colors[rand()], width: widths[rand()] };
14+
const initialData = { color: 'white', width: 200 };
1215

1316
/**
1417
* Components
1518
*/
1619
const ThemeHook = ({ title }: { title: string }) => {
1720
const [{ color }, { set: setColor }] = useColor();
18-
const [{ width }, { set: setWidth }] = useWidth();
21+
const [{ width }, { set: setWidth, resetAll }] = useWidth();
1922

2023
return (
2124
<div style={{ background: color, width }}>
@@ -24,6 +27,7 @@ const ThemeHook = ({ title }: { title: string }) => {
2427
<p>Width: {width}</p>
2528
<button onClick={() => setColor(colors[rand()])}>Change color</button>
2629
<button onClick={() => setWidth(widths[rand()])}>Change width</button>
30+
<button onClick={() => resetAll()}>Reset all</button>
2731
</div>
2832
);
2933
};

src/__tests__/integration.test.js

Lines changed: 153 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,7 @@ describe('Integration', () => {
181181

182182
const state3 = { loading: false, todos: ['todoB'] };
183183
const call3 = 3;
184-
// its 3+1 because on scope change we do NOT use context and force notify
185-
// causing ones that have naturally re-rendered already to re-render once more.
186-
expect(children1.mock.calls[call3 + 1]).toEqual([state3, expectActions]);
184+
expect(children1.mock.calls[call3]).toEqual([state3, expectActions]);
187185
expect(children2.mock.calls[call3]).toEqual([state3, expectActions]);
188186
});
189187

@@ -487,4 +485,156 @@ describe('Integration', () => {
487485
}).toThrow(/should be contained/);
488486
errorSpy.mockRestore();
489487
});
488+
489+
describe('dispatchTo', () => {
490+
const createTestElements = ({ mainContainer, otherContainer }) => {
491+
const actionOther =
492+
(n) =>
493+
({ setState }, { plus }) =>
494+
setState({ count: n, plus });
495+
const StoreOther = createStore({
496+
name: 'store-other',
497+
containedBy: otherContainer,
498+
initialState: {},
499+
actions: { set: actionOther },
500+
});
501+
const StoreMain = createStore({
502+
name: 'store-main',
503+
containedBy: mainContainer,
504+
initialState: {},
505+
actions: {
506+
setOther:
507+
(n) =>
508+
({ dispatchTo }) =>
509+
dispatchTo(StoreOther, actionOther(n)),
510+
},
511+
});
512+
513+
const MainSubscriber = createSubscriber(StoreMain);
514+
const OtherSubscriber = createSubscriber(StoreOther);
515+
const mainSpy = jest.fn().mockReturnValue(null);
516+
const otherSpy = jest.fn().mockReturnValue(null);
517+
518+
const Content = () => (
519+
<>
520+
<MainSubscriber>{mainSpy}</MainSubscriber>
521+
<OtherSubscriber>{otherSpy}</OtherSubscriber>
522+
</>
523+
);
524+
return {
525+
Content,
526+
StoreMain,
527+
mainReturn: (n = 0) => mainSpy.mock.calls[n],
528+
otherReturn: (n = 0) => otherSpy.mock.calls[n],
529+
};
530+
};
531+
532+
it('should allow dispatchTo global -> global', () => {
533+
const { Content, mainReturn, otherReturn } = createTestElements({
534+
mainContainer: null,
535+
otherContainer: null,
536+
});
537+
538+
render(<Content />);
539+
const [, mainActions] = mainReturn(0);
540+
act(() => mainActions.setOther(1));
541+
542+
expect(otherReturn(1)).toEqual([{ count: 1 }, expect.any(Object)]);
543+
});
544+
545+
it('should allow dispatchTo contained -> contained', () => {
546+
const SharedContainer = createContainer();
547+
const { Content, mainReturn, otherReturn } = createTestElements({
548+
mainContainer: SharedContainer,
549+
otherContainer: SharedContainer,
550+
});
551+
552+
render(
553+
<SharedContainer>
554+
<Content />
555+
</SharedContainer>
556+
);
557+
const [, mainActions] = mainReturn(0);
558+
act(() => mainActions.setOther(1));
559+
560+
expect(otherReturn(1)).toEqual([{ count: 1 }, expect.any(Object)]);
561+
});
562+
563+
it('should allow dispatchTo contained -> global', () => {
564+
const MainContainer = createContainer();
565+
const { Content, mainReturn, otherReturn } = createTestElements({
566+
mainContainer: MainContainer,
567+
otherContainer: null,
568+
});
569+
570+
render(
571+
<MainContainer>
572+
<Content />
573+
</MainContainer>
574+
);
575+
const [, mainActions] = mainReturn(0);
576+
act(() => mainActions.setOther(1));
577+
578+
expect(otherReturn(1)).toEqual([{ count: 1 }, expect.any(Object)]);
579+
});
580+
581+
it('should allow dispatchTo global -> contained if properly contained', () => {
582+
const OtherContainer = createContainer({ displayName: 'OtherContainer' });
583+
const { Content, mainReturn, otherReturn } = createTestElements({
584+
mainContainer: null,
585+
otherContainer: OtherContainer,
586+
});
587+
588+
render(
589+
<OtherContainer>
590+
<Content />
591+
</OtherContainer>
592+
);
593+
const [, mainActions] = mainReturn(0);
594+
act(() => mainActions.setOther(1));
595+
596+
expect(otherReturn(1)).toEqual([{ count: 1 }, expect.any(Object)]);
597+
});
598+
599+
it('should allow dispatchTo contained -> other contained', async () => {
600+
const MainContainer = createContainer();
601+
const OtherContainer = createContainer();
602+
603+
const { Content, mainReturn, otherReturn } = createTestElements({
604+
mainContainer: MainContainer,
605+
otherContainer: OtherContainer,
606+
});
607+
608+
render(
609+
<OtherContainer>
610+
<MainContainer>
611+
<Content />
612+
</MainContainer>
613+
</OtherContainer>
614+
);
615+
const [, mainActions] = mainReturn(0);
616+
act(() => mainActions.setOther(1));
617+
618+
expect(otherReturn(1)).toEqual([{ count: 1 }, expect.any(Object)]);
619+
});
620+
621+
it('should allow dispatchTo override -> contained', async () => {
622+
const { Content, StoreMain, mainReturn, otherReturn } =
623+
createTestElements({
624+
mainContainer: null,
625+
otherContainer: null,
626+
});
627+
const OverrideContainer = createContainer(StoreMain);
628+
629+
render(
630+
<OverrideContainer>
631+
<Content />
632+
</OverrideContainer>
633+
);
634+
const [, mainActions] = mainReturn(0);
635+
act(() => mainActions.setOther(1));
636+
637+
expect(otherReturn(1)).toEqual([{ count: 1 }, expect.any(Object)]);
638+
});
639+
});
490640
});

src/components/__tests__/container.test.js

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,6 @@ describe('Container', () => {
6060
describe('createContainer', () => {
6161
it('should return a Container component', () => {
6262
expect(Container.displayName).toEqual('Container(test)');
63-
expect(Container.storeType).toEqual(Store);
64-
expect(Container.hooks).toEqual({
65-
onInit: expect.any(Function),
66-
onUpdate: expect.any(Function),
67-
onCleanup: expect.any(Function),
68-
});
6963
});
7064
});
7165

@@ -88,7 +82,11 @@ describe('Container', () => {
8882
const children = <Subscriber>{() => null}</Subscriber>;
8983
render(<Container scope="s1">{children}</Container>);
9084

91-
expect(defaultRegistry.getStore).toHaveBeenCalledWith(Store, 's1');
85+
expect(defaultRegistry.getStore).toHaveBeenCalledWith(
86+
Store,
87+
's1',
88+
expect.any(Function)
89+
);
9290
expect(mockLocalRegistry.getStore).not.toHaveBeenCalled();
9391
});
9492

@@ -110,15 +108,23 @@ describe('Container', () => {
110108
</Container>
111109
);
112110

113-
expect(defaultRegistry.getStore).toHaveBeenCalledWith(Store, 's2');
111+
expect(defaultRegistry.getStore).toHaveBeenCalledWith(
112+
Store,
113+
's2',
114+
expect.any(Function)
115+
);
114116
});
115117

116118
it('should get local storeState if local matching', () => {
117119
const Subscriber = createSubscriber(Store);
118120
const children = <Subscriber>{() => null}</Subscriber>;
119121
render(<Container>{children}</Container>);
120122

121-
expect(mockLocalRegistry.getStore).toHaveBeenCalledWith(Store, undefined);
123+
expect(mockLocalRegistry.getStore).toHaveBeenCalledWith(
124+
Store,
125+
undefined,
126+
expect.any(Function)
127+
);
122128
expect(defaultRegistry.getStore).not.toHaveBeenCalled();
123129
});
124130

@@ -127,7 +133,11 @@ describe('Container', () => {
127133
const children = <Subscriber>{() => null}</Subscriber>;
128134
render(<Container isGlobal>{children}</Container>);
129135

130-
expect(defaultRegistry.getStore).toHaveBeenCalledWith(Store, undefined);
136+
expect(defaultRegistry.getStore).toHaveBeenCalledWith(
137+
Store,
138+
undefined,
139+
expect.any(Function)
140+
);
131141
expect(mockLocalRegistry.getStore).not.toHaveBeenCalled();
132142
});
133143

@@ -229,6 +239,7 @@ describe('Container', () => {
229239
setState: expect.any(Function),
230240
actions: expect.any(Object),
231241
dispatch: expect.any(Function),
242+
dispatchTo: expect.any(Function),
232243
},
233244
{ defaultCount: 5 }
234245
);
@@ -256,6 +267,7 @@ describe('Container', () => {
256267
setState: expect.any(Function),
257268
actions: expect.any(Object),
258269
dispatch: expect.any(Function),
270+
dispatchTo: expect.any(Function),
259271
},
260272
{ defaultCount: 6 }
261273
);

src/components/__tests__/hook.test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ describe('Hook', () => {
6161
it('should get the storeState from registry', () => {
6262
const { getRender } = setup();
6363
getRender();
64-
expect(defaultRegistry.getStore).toHaveBeenCalledWith(StoreMock);
64+
expect(defaultRegistry.getStore).toHaveBeenCalledWith(
65+
StoreMock,
66+
undefined,
67+
expect.any(Function)
68+
);
6569
});
6670

6771
it('should render children with store data and actions', () => {

0 commit comments

Comments
 (0)