Skip to content

Commit afc27d1

Browse files
committed
Added feature to call other stores
1 parent fdce20c commit afc27d1

File tree

3 files changed

+97
-31
lines changed

3 files changed

+97
-31
lines changed

README.MD

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,35 @@ await store.getTodos()
249249

250250
</p></details>
251251

252+
#### Calling other stores
253+
Sometimes you would like to call other store action from an action. You can't use `React.useContext` because of specific hook rules in React. Hook amount should never change during runtime and only way to supply that is to initialize all dependency contexts before bootstraping actions.
254+
255+
You can call actions in other stores by providing dependency contexts as a 3rd parameter to `createStoreContext`. Those contexts will be mapped to corresponding stores internally and will be available in `stores` object in `{setState, action, stores}` argument of action creator.
256+
257+
Example:
258+
```tsx
259+
const [todoContext, Provider] = createStoreContext({todos: []}, {
260+
addTodo: ({state, setState}, todo) => {
261+
setState({todos: [...state.todos, todo]})
262+
},
263+
})
264+
const [mainContext, Provider] = createStoreContext({message: ''}, {
265+
someAction: ({setState, stores}, name) => {
266+
const { todos } = stores.todoContext // stores.todoContext is a "todo store" ({todos: [], addTodo: (todo) => void})
267+
const newMessage = `Hello, ${name}, you have ${todos.length} todos!`
268+
setState({message: newMessage})
269+
},
270+
}, {todoContext})
271+
...
272+
// Usage
273+
const todoStore = React.useContext(todoContext)
274+
const mainStore = React.useContext(mainContext)
275+
todoStore.addTodo('buy milk')
276+
todoStore.addTodo('learn typescript')
277+
mainStore.someAction('Dmitrijs')
278+
// mainStore.message is "Hello, Dmitrijs, you have 2 todos!"
279+
```
280+
252281
### TODO:
253282
* Documentation sections about what benefits does TypeScript gives
254283
* Full example projects

src/index.tsx

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,42 +24,71 @@ export type Tail<T> = T extends Array<any>
2424
API Types
2525
*/
2626

27-
type ContextReference<TState> = {
27+
type ContextReference<TState, TContexts extends Contexts> = {
2828
state: TState
2929
setState: React.Dispatch<React.SetStateAction<TState>>
30+
stores: InferStores<TContexts>
3031
}
3132

32-
type Action<TState, TArgs extends never[]= never[], TReturn = any> =
33-
(contextReference: ContextReference<TState>, ...args: TArgs) => TReturn
33+
type Action<TState, TContexts extends Contexts, TArgs extends never[]= never[], TReturn = any> =
34+
(contextReference: ContextReference<TState, TContexts>, ...args: TArgs) => TReturn
3435

35-
type Actions<TState> = { [key: string]: Action<TState> }
36+
type Actions<TState, TContexts extends Contexts> = { [key: string]: Action<TState, TContexts> }
3637

37-
type MappedActions<TState, TActions extends Actions<TState>> = {
38+
type MappedActions<TState, TContexts extends Contexts, TActions extends Actions<TState, TContexts>> = {
3839
[P in keyof TActions]: (...args: DropFirst<Params<TActions[P]>>) => RetType<TActions[P]>
3940
}
4041

41-
type Store<TState, TActions extends Actions<TState>> =
42-
TActions extends undefined ? TState : TState & MappedActions<TState, TActions>
42+
type Store<TState, TContexts extends Contexts, TActions extends Actions<TState, TContexts>> =
43+
TActions extends undefined ? TState : TState & MappedActions<TState, TContexts, TActions>
4344

45+
type Context<T = any> = React.Context<T>
46+
type Contexts = { [key: string]: Context }
47+
48+
export type Params1<T extends (...args: never[]) => unknown> = T extends (...args: infer P) => any ? P : never;
49+
50+
type InferStore<TContext extends React.Context<any>> =
51+
TContext extends React.Context<infer TStore> ? TStore : never
52+
53+
type InferStores<TContexts extends Contexts> = {
54+
[P in keyof TContexts]: InferStore<TContexts[P]>
55+
}
4456
/*
4557
Logic
4658
*/
4759

48-
export default function createStoreContext<TState, TActions extends Actions<TState> = {}>(
49-
initialState: TState,
50-
actions?: TActions
51-
): [React.Context<Store<TState, TActions>>, React.FC] {
52-
const store = { ...initialState, ...mapActionsToDefault(initialState, actions) } as Store<TState, TActions>
60+
export default function createStoreContext<
61+
TState,
62+
TContexts extends Contexts,
63+
TActions extends Actions<TState, TContexts> = {},
64+
>(
65+
initialState: TState,
66+
actions: TActions = {} as TActions,
67+
contexts: TContexts = {} as TContexts
68+
): [React.Context<Store<TState, TContexts, TActions>>, React.FC] {
69+
// tslint:disable-next-line: max-line-length
70+
const store = { ...initialState, ...mapActionsToDefault(initialState, actions) } as Store<TState, TContexts, TActions>
5371
const context = React.createContext(store)
5472

5573
const provider: React.FC = props => {
5674
let [_state, setState] = React.useState(initialState)
75+
76+
const stores = Object.keys(contexts).reduce(
77+
(obj, key) => {
78+
return {
79+
...obj,
80+
[key]: React.useContext(contexts[key])
81+
}
82+
},
83+
{} as InferStores<TContexts>)
84+
5785
const _actions = mapActionsToDispatch({
5886
state: _state,
59-
setState
87+
setState,
88+
stores
6089
}, actions)
6190

62-
const _store = { ..._state, ..._actions } as Store<TState, TActions>
91+
const _store = { ..._state, ..._actions } as Store<TState, TContexts, TActions>
6392

6493
return (
6594
<context.Provider value={_store}>
@@ -71,37 +100,42 @@ export default function createStoreContext<TState, TActions extends Actions<TSta
71100
return [context, provider]
72101
}
73102

74-
function mapActionsToDispatch<TState, TActions extends Actions<TState>>(
75-
contextReference: ContextReference<TState>,
103+
function mapActionsToDispatch<TState, TContexts extends Contexts, TActions extends Actions<TState, TContexts>>(
104+
contextReference: ContextReference<TState, TContexts>,
76105
actions?: TActions,
77-
): MappedActions<TState, TActions> {
78-
if (actions === undefined) return {} as MappedActions<TState, TActions>
106+
): MappedActions<TState, TContexts, TActions> {
107+
if (actions === undefined) return {} as MappedActions<TState, TContexts, TActions>
79108
return Object.keys(actions).reduce(
80109
(obj, key) => {
81110
return {
82111
...obj,
83112
[key]: (...args: never[]) => actions[key](contextReference, ...args)
84113
}
85114
},
86-
{} as MappedActions<TState, TActions>)
115+
{} as MappedActions<TState, TContexts, TActions>)
87116
}
88117

89-
function mapActionsToDefault<TState, TActions extends Actions<TState>>(
118+
function mapActionsToDefault<TState,
119+
TActions extends Actions<TState, TContexts>,
120+
TContexts extends Contexts
121+
>(
90122
initialState: TState,
91123
actions?: TActions,
92-
): MappedActions<TState, TActions> {
93-
if (actions === undefined) return {} as MappedActions<TState, TActions>
124+
): MappedActions<TState, TContexts, TActions> {
125+
if (actions === undefined) return {} as MappedActions<TState, TContexts, TActions>
94126

95127
return Object.keys(actions).reduce(
96128
(obj, key) => {
97-
const contextReference: ContextReference<TState> = {
129+
const contextReference: ContextReference<TState, TContexts> = {
98130
state: initialState,
131+
stores: {} as InferStores<TContexts>,
132+
// tslint:disable-next-line: max-line-length
99133
setState: (value) => { throw new Error(`[${key}]: Can't invoke 'setState' with ${value} because provider does not exist`) }
100134
}
101135
return {
102136
...obj,
103137
[key]: (...args: never[]) => actions[key](contextReference, ...args)
104138
}
105139
},
106-
{} as MappedActions<TState, TActions>)
140+
{} as MappedActions<TState, TContexts, TActions>)
107141
}

tests/tests.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -231,22 +231,25 @@ describe('Logic', () => {
231231

232232
it('can call other context from action', () => {
233233
const [context1, Provider1] = createContextState({ state1: 'before' }, {
234-
action1: () => {
234+
action1: ({ setState }) => {
235235
return 'value-from-action1'
236236
},
237237
})
238238

239239
const [context2, Provider2] = createContextState({ state2: 'before' }, {
240-
action2: (_) => {
241-
const store = React.useContext(context1)
242-
return store.action1()
240+
action2: ({ stores }) => {
241+
return stores.context1.action1()
243242
},
244-
})
243+
}, { context1 })
245244

246245
const Consumer: React.FC = props => {
247-
const store = React.useContext(context2)
246+
const store1 = React.useContext(context1)
247+
const store2 = React.useContext(context2)
248+
249+
const result = store2.action2()
248250

249-
expect(store.action2()).toBe('value-from-action1')
251+
//expect(store1.state1).toBe('after')
252+
expect(result).toBe('value-from-action1')
250253

251254
verify()
252255

0 commit comments

Comments
 (0)