Skip to content

Commit 17183b6

Browse files
Refactor dispatch and getStore naming (#203)
1 parent 0350909 commit 17183b6

9 files changed

Lines changed: 108 additions & 94 deletions

File tree

src/components/__tests__/integration.test.js renamed to src/__tests__/integration.test.js

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import React, { Fragment, memo, useEffect } from 'react';
55
import { render } from '@testing-library/react';
66
import { act } from 'react-dom/test-utils';
77

8-
import { createStore, defaultRegistry } from '../../store';
9-
import { createContainer } from '../container';
10-
import { createSubscriber } from '../subscriber';
11-
import { createHook } from '../hook';
8+
import { createStore, defaultRegistry } from '../store';
9+
import { createContainer } from '../components/container';
10+
import { createSubscriber } from '../components/subscriber';
11+
import { createHook } from '../components/hook';
1212

1313
const actTick = () => act(async () => await Promise.resolve());
1414

@@ -471,10 +471,8 @@ describe('Integration', () => {
471471
expect(handlers2.onDestroy).toHaveBeenCalledTimes(1);
472472
});
473473

474-
it('should throw an error if contained store is used without container', async () => {
475-
const rejectSpy = jest
476-
.spyOn(Promise, 'reject')
477-
.mockImplementation(() => {});
474+
it('should throw an error if contained store is used without container', () => {
475+
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
478476
const Store1 = createStore({
479477
name: 'one',
480478
initialState: { todos: [], loading: false },
@@ -483,12 +481,10 @@ describe('Integration', () => {
483481
});
484482

485483
const Subscriber = createSubscriber(Store1);
486-
render(<Subscriber>{() => null}</Subscriber>);
487-
await actTick();
488484

489-
expect(rejectSpy).toHaveBeenCalled();
490-
const [error] = rejectSpy.mock.calls[0];
491-
expect(error).toEqual(expect.any(Error));
492-
expect(error.message).toContain('should be contained');
485+
expect(() => {
486+
render(<Subscriber>{() => null}</Subscriber>);
487+
}).toThrow(/should be contained/);
488+
errorSpy.mockRestore();
493489
});
494490
});

src/components/__tests__/container.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-env jest */
22

33
import React from 'react';
4-
import { render } from '@testing-library/react';
4+
import { render, act } from '@testing-library/react';
55

66
import { StoreMock, storeStateMock } from '../../__tests__/mocks';
77
import { defaultRegistry, StoreRegistry } from '../../store/registry';
@@ -269,7 +269,7 @@ describe('Container', () => {
269269
const children = <Subscriber>{renderPropChildren}</Subscriber>;
270270
render(<Container defaultCount={5}>{children}</Container>);
271271
const [, { increase }] = renderPropChildren.mock.calls[0];
272-
increase();
272+
act(() => increase());
273273

274274
expect(actionInner).toHaveBeenCalledWith(expect.any(Object), {
275275
defaultCount: 5,
@@ -287,7 +287,7 @@ describe('Container', () => {
287287
);
288288
const [, { increase }] = renderPropChildren.mock.calls[0];
289289
rerender(<Container defaultCount={6}>{children}</Container>);
290-
increase();
290+
act(() => increase());
291291

292292
expect(actionInner).toHaveBeenCalledWith(expect.any(Object), {
293293
defaultCount: 6,

src/components/container.js

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,16 @@ export default class Container extends Component {
4949
// These fallbacks are needed only to make enzyme shallow work
5050
// as it does not fully support provider-less Context enzyme#1553
5151
globalRegistry = defaultRegistry,
52-
getStore = defaultRegistry.getStore,
52+
retrieveStore = defaultRegistry.getStore,
5353
} = this.context;
5454

5555
this.state = {
5656
api: {
5757
globalRegistry,
58-
getStore: (Store, scope) =>
58+
retrieveStore: (Store, scope) =>
5959
this.constructor.storeType === Store
6060
? this.getScopedStore(scope)
61-
: getStore(Store),
61+
: retrieveStore(Store),
6262
},
6363
// stored to make them available in getDerivedStateFromProps
6464
// as js context there is null https://github.com/facebook/react/issues/12612
@@ -103,20 +103,15 @@ export default class Container extends Component {
103103
const { storeType, hooks } = this.constructor;
104104
const { api } = this.state;
105105
// we explicitly pass scope as it might be changed
106-
const { storeState } = api.getStore(storeType, scope);
106+
const { storeState } = api.retrieveStore(storeType, scope);
107107

108-
const actions = bindActions(
109-
storeType.actions,
110-
storeState,
111-
this.getContainerProps
112-
);
108+
const config = {
109+
props: () => this.actionProps,
110+
contained: (s) => storeType === s,
111+
};
113112

114-
this.scopedHooks = bindActions(
115-
hooks,
116-
storeState,
117-
this.getContainerProps,
118-
actions
119-
);
113+
const actions = bindActions(storeType.actions, storeState, config);
114+
this.scopedHooks = bindActions(hooks, storeState, config, actions);
120115

121116
// make sure we also reset actionProps
122117
this.actionProps = null;
@@ -146,8 +141,6 @@ export default class Container extends Component {
146141
return restProps;
147142
};
148143

149-
getContainerProps = () => this.actionProps;
150-
151144
getRegistry() {
152145
const isLocal = !this.props.scope && !this.props.isGlobal;
153146
return isLocal ? this.registry : this.state.api.globalRegistry;
@@ -224,15 +217,15 @@ function useRegistry(scope, isGlobal, { globalRegistry }) {
224217
}, [scope, isGlobal, globalRegistry]);
225218
}
226219

227-
function useContainedStore(scope, registry, props, override) {
220+
function useContainedStore(scope, registry, props, check, override) {
228221
// Store contained scopes in a map, but throwing it away on scope change
229222
// eslint-disable-next-line react-hooks/exhaustive-deps
230223
const containedStores = useMemo(() => new Map(), [scope]);
231224

232225
// Store props in a ref to avoid re-binding actions when they change and re-rendering all
233226
// consumers unnecessarily. The update is handled by an effect on the component instead
234-
const containerProps = useRef();
235-
containerProps.current = props;
227+
const propsRef = useRef();
228+
propsRef.current = props;
236229

237230
const getContainedStore = useCallback(
238231
(Store) => {
@@ -241,13 +234,12 @@ function useContainedStore(scope, registry, props, override) {
241234
// so we can provide props to actions (only triggered by children)
242235
if (!containedStore) {
243236
const isExisting = registry.hasStore(Store, scope);
244-
const { storeState } = registry.getStore(Store, scope, true);
245-
const getProps = () => containerProps.current;
246-
const actions = bindActions(Store.actions, storeState, getProps);
237+
const config = { props: () => propsRef.current, contained: check };
238+
const { storeState, actions } = registry.getStore(Store, scope, config);
247239
const handlers = bindActions(
248240
{ ...Store.handlers, ...override?.handlers },
249241
storeState,
250-
getProps,
242+
config,
251243
actions
252244
);
253245
containedStore = {
@@ -263,20 +255,20 @@ function useContainedStore(scope, registry, props, override) {
263255
}
264256
return containedStore;
265257
},
266-
[containedStores, registry, scope, override]
258+
[containedStores, registry, scope, check, override]
267259
);
268260
return [containedStores, getContainedStore];
269261
}
270262

271-
function useApi(check, getContainedStore, { globalRegistry, getStore }) {
272-
const getStoreRef = useRef();
273-
getStoreRef.current = (Store) =>
274-
check(Store) ? getContainedStore(Store) : getStore(Store);
263+
function useApi(check, getContainedStore, { globalRegistry, retrieveStore }) {
264+
const retrieveRef = useRef();
265+
retrieveRef.current = (Store) =>
266+
check(Store) ? getContainedStore(Store) : retrieveStore(Store);
275267

276268
// This api is "frozen", as changing it will trigger re-render across all consumers
277-
// so we link getStore dynamically and manually call notify() on scope change
269+
// so we link retrieveStore dynamically and manually call notify() on scope change
278270
return useMemo(
279-
() => ({ globalRegistry, getStore: (s) => getStoreRef.current(s) }),
271+
() => ({ globalRegistry, retrieveStore: (s) => retrieveRef.current(s) }),
280272
[globalRegistry]
281273
);
282274
}
@@ -294,6 +286,7 @@ function createFunctionContainer({ displayName, override } = {}) {
294286
scope,
295287
registry,
296288
restProps,
289+
check,
297290
override
298291
);
299292
const api = useApi(check, getContainedStore, ctx);
@@ -329,7 +322,7 @@ function createFunctionContainer({ displayName, override } = {}) {
329322
!storeState.listeners().size &&
330323
// ensure registry has not already created a new store with same scope
331324
storeState ===
332-
registry.getStore(Store, cachedScope, true).storeState
325+
registry.getStore(Store, cachedScope, null)?.storeState
333326
) {
334327
handlers.onDestroy?.();
335328
registry.deleteStore(Store, cachedScope);

src/components/hook.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ const DEFAULT_SELECTOR = (state) => state;
99

1010
export function createHook(Store, { selector } = {}) {
1111
return function useSweetState(propsArg) {
12-
const { getStore } = useContext(Context);
13-
const { storeState, actions } = getStore(Store);
12+
const { retrieveStore } = useContext(Context);
13+
const { storeState, actions } = retrieveStore(Store);
1414

1515
const hasPropsArg = propsArg !== undefined;
1616
const propsArgRef = useRef(propsArg);
@@ -27,9 +27,9 @@ export function createHook(Store, { selector } = {}) {
2727
);
2828

2929
const getSnapshot = useCallback(() => {
30-
const state = getStore(Store).storeState.getState();
30+
const state = retrieveStore(Store).storeState.getState();
3131
return stateSelector(state, propsArgRef.current);
32-
}, [getStore, stateSelector]);
32+
}, [retrieveStore, stateSelector]);
3333

3434
const currentState = useSyncExternalStore(
3535
storeState.subscribe,

src/context.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { defaultRegistry } from './store';
77
export const Context = React.createContext(
88
{
99
globalRegistry: defaultRegistry,
10-
getStore: defaultRegistry.getStore,
10+
retrieveStore: (Store) => defaultRegistry.getStore(Store),
1111
},
1212
() => 0
1313
);

src/store/__tests__/bind-actions.test.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import defaults from '../../defaults';
66

77
jest.mock('../../defaults');
88

9+
const createConfigArg = ({ props = {} } = {}) => ({
10+
props: () => props,
11+
contained: () => false,
12+
});
13+
914
describe('bindAction', () => {
1015
it('should bind and call providing mutator, getState and container props', () => {
1116
const actionInner = jest.fn();
@@ -16,7 +21,7 @@ describe('bindAction', () => {
1621
storeStateMock,
1722
action,
1823
'myAction',
19-
() => containerProps,
24+
createConfigArg({ props: containerProps }),
2025
actionsMock
2126
);
2227
result(1, '2', 3);
@@ -44,7 +49,12 @@ describe('bindAction', () => {
4449
() =>
4550
({ setState }) =>
4651
setState();
47-
const result = bindAction(storeStateMock, action, 'myAction');
52+
const result = bindAction(
53+
storeStateMock,
54+
action,
55+
'myAction',
56+
createConfigArg()
57+
);
4858
result();
4959

5060
expect(storeStateMock.mutator.actionName).toEqual('myAction');
@@ -60,7 +70,12 @@ describe('bindAction', () => {
6070
() =>
6171
({ dispatch }) =>
6272
dispatch(action2());
63-
const result = bindAction(storeStateMock, action, 'myAction2');
73+
const result = bindAction(
74+
storeStateMock,
75+
action,
76+
'myAction2',
77+
createConfigArg()
78+
);
6479
result();
6580

6681
expect(storeStateMock.mutator.actionName).toEqual('myAction2.dispatch');
@@ -69,7 +84,7 @@ describe('bindAction', () => {
6984

7085
describe('bindActions', () => {
7186
it('should return all actions bound', () => {
72-
const result = bindActions(actionsMock, storeStateMock);
87+
const result = bindActions(actionsMock, storeStateMock, createConfigArg());
7388
expect(result).toEqual({
7489
increase: expect.any(Function),
7590
decrease: expect.any(Function),
@@ -83,7 +98,7 @@ describe('bindActions', () => {
8398
dispatch(actionsMock.decrease())
8499
);
85100
actionsMock.decrease.mockReturnValue(({ getState }) => getState());
86-
const result = bindActions(actionsMock, storeStateMock);
101+
const result = bindActions(actionsMock, storeStateMock, createConfigArg());
87102
const output = result.increase();
88103

89104
expect(output).toEqual(state);

src/store/__tests__/registry.test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@ describe('StoreRegistry', () => {
2020
expect(instance.storeState.getState()).toEqual({ count: 0 });
2121
});
2222

23+
it('should only return store if no config', () => {
24+
const registry = new StoreRegistry();
25+
const instance = registry.getStore(StoreMock, 's1', null);
26+
expect(instance).toEqual(null);
27+
});
28+
29+
it('should error if not contained', () => {
30+
const registry = new StoreRegistry();
31+
const ContainedStore = { ...StoreMock, containedBy: jest.fn() };
32+
expect(() => {
33+
registry.getStore(ContainedStore, 's1');
34+
}).toThrow(/should be contained/);
35+
});
36+
2337
it('should say if store exists already', () => {
2438
const registry = new StoreRegistry();
2539
expect(registry.hasStore(StoreMock, 's1')).toBe(false);

src/store/bind-actions.js

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,50 +13,39 @@ export const bindAction = (
1313
storeState,
1414
actionFn,
1515
actionKey,
16-
getContainerProps = () => {},
17-
boundActions = {}
16+
config,
17+
actions
1818
) => {
19-
// Setting mutator name so we can log action name for better debuggability
20-
const dispatch = (thunkFn, actionName = `${actionKey}.dispatch`) =>
19+
const callThunk = (instance, thunkFn, actionName) =>
2120
thunkFn(
2221
{
2322
setState: defaults.devtools
24-
? namedMutator(storeState, actionName)
25-
: storeState.mutator,
26-
getState: storeState.getState,
23+
? namedMutator(instance.storeState, actionName)
24+
: instance.storeState.mutator,
25+
getState: instance.storeState.getState,
2726
get actions() {
2827
if (!warnings.has(actionFn)) {
2928
warnings.set(
3029
actionFn,
3130
console.warn(
3231
`react-sweet-state 'actions' property has been deprecated and will be removed in the next mayor. ` +
33-
`Please check action '${actionName}' of Store '${storeState.key}' and use 'dispatch' instead`
32+
`Please check action '${actionName}' of Store '${instance.storeState.key}' and use 'dispatch' instead`
3433
)
3534
);
3635
}
3736

38-
return boundActions;
37+
return actions;
3938
},
40-
dispatch,
39+
dispatch: (tFn) => callThunk(instance, tFn, `${actionName}.dispatch`),
4140
},
42-
getContainerProps()
41+
config.props()
4342
);
44-
return (...args) => dispatch(actionFn(...args), actionKey);
43+
return (...args) =>
44+
callThunk({ storeState, actions }, actionFn(...args), actionKey);
4545
};
4646

47-
export const bindActions = (
48-
actions,
49-
storeState,
50-
getContainerProps = () => ({}),
51-
boundActions = null
52-
) =>
47+
export const bindActions = (actions, storeState, config, boundActions = null) =>
5348
Object.keys(actions).reduce((acc, k) => {
54-
acc[k] = bindAction(
55-
storeState,
56-
actions[k],
57-
k,
58-
getContainerProps,
59-
boundActions || acc
60-
);
49+
acc[k] = bindAction(storeState, actions[k], k, config, boundActions || acc);
6150
return acc;
6251
}, {});

0 commit comments

Comments
 (0)