Skip to content

Commit 52742d0

Browse files
committed
feat(store): consolidate store vs storeBuilder and allow for lazy creation of global store instance, without calling .create()
1 parent 7acaa7b commit 52742d0

File tree

8 files changed

+195
-132
lines changed

8 files changed

+195
-132
lines changed

packages/store/src/store.tsx

Lines changed: 34 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
/* eslint-disable no-unused-vars */
22

3-
import {
4-
EffectBuilder,
5-
State,
6-
StoreApi,
7-
StoreBuilderApi,
8-
StoreDef,
9-
} from './types';
3+
import { EffectBuilder, State, StoreApi, StoreDef } from './types';
104
import { StoreOptions } from './types/CreateStoreOptions';
115

126
import React from 'react';
@@ -17,8 +11,12 @@ import {
1711
} from './utils/create-computed-methods';
1812
import { createStore } from './utils/create-inner-store';
1913
import { createSplitProps } from './utils/split-props';
14+
import { createStoreApiProxy } from './utils/create-store-proxy';
2015

21-
export const storeBuilder = <TState extends State>() => {
16+
export const store = <TState extends State>(
17+
initialState?: TState,
18+
options?: StoreOptions<TState>
19+
): StoreApi<TState> => {
2220
const _def = {
2321
initialState: undefined,
2422
input: {},
@@ -39,25 +37,23 @@ export const storeBuilder = <TState extends State>() => {
3937
) {
4038
_def.extensions.push(builder);
4139

42-
if (_def.options.onExtend) {
43-
return _def.options.onExtend(builder);
44-
}
45-
return builderMethods;
40+
return storeApi;
4641
}
4742

48-
const builderMethods = {
43+
const storeApi = createStoreApiProxy({
44+
_def,
4945
options: (newOpts: any) => {
5046
Object.assign(_def.options, newOpts);
51-
return builderMethods;
47+
return storeApi;
5248
},
5349
state: (initialValue: TState) => {
5450
Object.assign(_def, { initialState: initialValue });
55-
return builderMethods;
51+
return storeApi;
5652
},
5753

5854
name: (newName: string) => {
5955
Object.assign(_def.options, { name: newName });
60-
return builderMethods;
56+
return storeApi;
6157
},
6258

6359
/**
@@ -71,7 +67,6 @@ export const storeBuilder = <TState extends State>() => {
7167
effects: <TBuilder extends EffectBuilder<StoreApi<TState, {}>>>(
7268
builder: TBuilder
7369
): StoreApi<TState, {}> => {
74-
// @ts-expect-error
7570
return extend((store) => {
7671
const effectNameToFn = builder(store);
7772
const unsubMethods: Record<string, () => void> = {};
@@ -93,15 +88,11 @@ export const storeBuilder = <TState extends State>() => {
9388
unsubscribeFromEffects,
9489
};
9590

96-
// subscribe to the effects when the store is created
97-
subscribeToEffects();
98-
9991
return extraProps;
10092
});
10193
},
10294
computed: <TComputedProps extends ComputedProps>(
10395
computedCallback: ComputedBuilder<TState, TComputedProps>
104-
// ): StoreApi<TState, TComputedProps> => {
10596
) => {
10697
return extend((store) =>
10798
// @ts-expect-error
@@ -110,7 +101,10 @@ export const storeBuilder = <TState extends State>() => {
110101
},
111102
create: (initialValue: Partial<TState> & Record<string, any>) => {
112103
if (!initialValue) {
113-
return createStore(_def);
104+
const instance = createStore(_def);
105+
106+
Object.assign(instance, storeApi);
107+
return instance;
114108
}
115109

116110
if (!isObject(_def.initialState)) {
@@ -130,90 +124,24 @@ export const storeBuilder = <TState extends State>() => {
130124
// @ts-expect-error
131125
return createStore(_def, stateInitialValue, inputInitialValue);
132126
},
133-
};
134-
135-
return Object.assign(builderMethods, {
136-
_def,
137-
}) as unknown as StoreBuilderApi<TState>;
138-
};
139-
140-
export const store = <TState extends State>(
141-
initialState?: TState,
142-
options?: StoreOptions<TState>
143-
): StoreBuilderApi<TState> => {
144-
let globalStore = {} as unknown as StoreBuilderApi<TState>;
145-
146-
const _options = {
147-
...options,
148-
onExtend: (builder: any) => {
149-
if (options?.onExtend) {
150-
options.onExtend(store);
151-
}
152-
153-
Object.assign(globalStore, builder(globalStore));
154-
return globalStore as unknown as StoreApi<TState, {}>;
155-
},
156-
};
127+
});
157128

129+
// must check for undefined to allow for 0 as a valid initial state
158130
if (initialState !== undefined) {
159-
const builder = storeBuilder()
160-
.options(_options as any)
161-
.state(initialState);
162-
163-
const instance = builder.create() as StoreApi<TState>;
164-
165-
Object.assign(instance, builder);
166-
167-
// @ts-expect-error
168-
globalStore = instance;
169-
} else {
170-
const stateFn = (_initialState: any) => {
171-
const builder = storeBuilder()
172-
.options(_options as any)
173-
.state(_initialState);
174-
175-
const instance = builder.create() as StoreApi<TState>;
176-
177-
Object.assign(instance, builder);
178-
179-
// @ts-expect-error
180-
globalStore = instance;
181-
return globalStore as StoreBuilderApi<TState>;
182-
};
183-
184-
// @ts-expect-error
185-
globalStore = {
186-
state: stateFn,
187-
};
131+
Object.assign(_def, { initialState });
188132
}
189133

190-
return globalStore as unknown as StoreBuilderApi<TState>;
191-
192-
// return storeBuilder()
193-
// .state(initialState)
194-
// .options(options ?? {})
195-
// .create() as StoreApi<TState>;
196-
197-
// if (initialState !== undefined && options !== undefined)
198-
// return storeBuilder()
199-
// .options(options as any)
200-
// .state(initialState) as StoreApi<TState>;
201-
202-
// if (initialState !== undefined) {
203-
// return storeBuilder().state(initialState).create() as StoreApi<TState>;
204-
// }
205-
206-
// if (options !== undefined) {
207-
// return storeBuilder().options(options as any) as StoreApi<TState>;
208-
// }
134+
if (options) {
135+
Object.assign(_def, { options });
136+
}
209137

210-
// return storeBuilder() as StoreApi<TState>;
138+
return storeApi as unknown as StoreApi<TState>;
211139
};
212140

213141
export function createStoreContext<
214142
TState extends State,
215143
TExtensions extends object,
216-
>(store: StoreBuilderApi<TState, TExtensions>) {
144+
>(store: StoreApi<TState, TExtensions>) {
217145
const Context = React.createContext<StoreApi<TState, TExtensions> | null>(
218146
null
219147
);
@@ -230,13 +158,17 @@ export function createStoreContext<
230158
);
231159

232160
React.useEffect(() => {
161+
const instance = storeInstance.current;
162+
if (instance && 'subscribeToEffects' in instance) {
163+
const fn = instance.subscribeToEffects;
164+
if (typeof fn === 'function') fn();
165+
}
166+
233167
return () => {
234-
if (
235-
storeInstance.current &&
236-
'unsubscribeFromEffects' in storeInstance.current
237-
) {
238-
// @ts-expect-error
239-
storeInstance.current?.unsubscribeFromEffects?.();
168+
const instance = storeInstance.current;
169+
if (instance && 'unsubscribeFromEffects' in instance) {
170+
const fn = instance.unsubscribeFromEffects;
171+
if (typeof fn === 'function') fn();
240172
}
241173
};
242174
}, []);

packages/store/src/types.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,22 @@ export interface StoreDef<
2222
initialState: TState | undefined;
2323
}
2424

25-
export type StoreBuilderApi<
25+
export type StoreBuilderMethods<
2626
TState extends State,
2727
TExtendedProps extends Record<string, any> = {},
2828
TInput extends Record<string, any> = {},
29-
> = StoreApi<TState, TExtendedProps, TInput> & {
29+
> = {
3030
_def: StoreDef<TState, TExtendedProps>;
3131

3232
input: <TNewInput extends Record<string, any>>(
3333
initialInput: TNewInput
34-
) => StoreBuilderApi<TState, TExtendedProps, TNewInput>;
34+
) => StoreApi<TState, TExtendedProps, TNewInput>;
3535
options: (
3636
options: StoreOptions<TState>
37-
) => StoreBuilderApi<TState, TExtendedProps, TInput>;
37+
) => StoreApi<TState, TExtendedProps, TInput>;
3838
state: <TNewState>(
3939
initialValue: TNewState
40-
) => StoreBuilderApi<TNewState, TExtendedProps, TInput>;
40+
) => StoreApi<TNewState, TExtendedProps, TInput>;
4141

4242
/**
4343
* Creates a new store with the given initial value
@@ -60,7 +60,7 @@ export type StoreBuilderApi<
6060
TBuilder extends ExtendBuilder<StoreApi<TState, TExtendedProps, TInput>>,
6161
>(
6262
builder: TBuilder
63-
): StoreBuilderApi<TState, TExtendedProps & ReturnType<TBuilder>, TInput>;
63+
): StoreApi<TState, TExtendedProps & ReturnType<TBuilder>, TInput>;
6464
/**
6565
* Extends the store
6666
* @param builder a callback that receives the store and returns an object with the new methods
@@ -69,29 +69,27 @@ export type StoreBuilderApi<
6969
TBuilder extends ExtendBuilder<StoreApi<TState, TExtendedProps, TInput>>,
7070
>(
7171
builder: TBuilder
72-
): StoreBuilderApi<TState, TExtendedProps & ReturnType<TBuilder>, TInput>;
72+
): StoreApi<TState, TExtendedProps & ReturnType<TBuilder>, TInput>;
7373

7474
computed<
7575
TComputedProps extends ComputedProps,
7676
TBuilder extends ComputedBuilder<
77-
StoreBuilderApi<TState, TExtendedProps, TInput>,
77+
StoreApi<TState, TExtendedProps, TInput>,
7878
TComputedProps
7979
>,
8080
>(
8181
builder: TBuilder
82-
): StoreBuilderApi<
82+
): StoreApi<
8383
TState,
8484
TExtendedProps & ComputedMethods<ReturnType<TBuilder>>,
8585
TInput
8686
>;
8787

8888
effects<
89-
TBuilder extends EffectBuilder<
90-
StoreBuilderApi<TState, TExtendedProps, TInput>
91-
>,
89+
TBuilder extends EffectBuilder<StoreApi<TState, TExtendedProps, TInput>>,
9290
>(
9391
builder: TBuilder
94-
): StoreBuilderApi<
92+
): StoreApi<
9593
TState,
9694
TExtendedProps & EffectMethods<ReturnType<TBuilder>>,
9795
TInput
@@ -102,7 +100,9 @@ export type StoreApi<
102100
TState extends State = {},
103101
TExtendedProps extends Record<string, any> = {},
104102
TInput extends Record<string, any> = {},
105-
> = NestedStoreMethods<TState> & Simplify<TExtendedProps & TInput>;
103+
> = StoreBuilderMethods<TState, TExtendedProps, TInput> &
104+
NestedStoreMethods<TState> &
105+
Simplify<TExtendedProps & TInput>;
106106

107107
export type EffectBuilder<TStore extends StoreApi<any, any, any>> = (
108108
store: TStore

packages/store/src/types/CreateStoreOptions.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ export interface StoreOptions<T extends State> {
2525
*/
2626
persist?: PersistOptions<T>;
2727

28-
onExtend?: (builder: any) => void;
28+
/**
29+
* If mode is "CREATE" then the store will defined and created in the same step.
30+
*
31+
* If mode is "DEFINE" then the store will only be defined, and you will need to call the create method to create the store.
32+
*/
33+
// mode?: 'CREATE' | 'DEFINE';
2934
}
3035

3136
export interface ImmerOptions {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/* eslint-disable no-unused-vars */
2+
import { StoreApi } from '../types';
3+
import { createStore } from './create-inner-store';
4+
5+
/**
6+
* allows for lazy .create() of the store
7+
*
8+
* If the store is not created, it will automatically create the store instance
9+
*
10+
* However, if you want to define a store without creating it eg only for context, it won't unnecessarily create the store instance
11+
*/
12+
export const createStoreApiProxy = <TStore extends StoreApi<any, any, any>>(
13+
storeApi: Partial<TStore>
14+
) => {
15+
let instance: any;
16+
17+
const proxy: unknown = new Proxy(instance ?? storeApi, {
18+
get(target, key, receiver) {
19+
if (typeof key !== 'string' || key === 'then') {
20+
return undefined;
21+
}
22+
23+
// how this could be made more precise:
24+
// IF key is one of the store builder methods (eg extend) then pass through
25+
// ELSE IF key is one of the store methods (eg get) OR key of initial state then create the store instance
26+
// else return undefined
27+
28+
// with the current implementation there is some potential for bugs but so far it's working fine so leaving it as is.
29+
30+
const isActualKeyOfTarget = key in target && !excludedKeys.includes(key);
31+
if (isActualKeyOfTarget) {
32+
return Reflect.get(target, key, receiver);
33+
}
34+
35+
// automatically create the global store instance
36+
if (!instance) {
37+
// @ts-expect-error
38+
// instance = storeApi.create();
39+
instance = createStore(storeApi._def);
40+
Object.assign(instance, storeApi);
41+
42+
// because it's the global store we want to call the subscribeToEffects method
43+
if ('subscribeToEffects' in instance) {
44+
const fn = instance.subscribeToEffects;
45+
if (typeof fn === 'function') fn();
46+
}
47+
// (for the context stores we call them inside useEffect instead)
48+
}
49+
50+
return instance[key];
51+
},
52+
});
53+
54+
return instance ?? (proxy as unknown as TStore);
55+
};
56+
const excludedKeys = [
57+
'constructor',
58+
'prototype',
59+
'__proto__',
60+
'toString',
61+
'valueOf',
62+
'toLocaleString',
63+
'hasOwnProperty',
64+
'isPrototypeOf',
65+
'propertyIsEnumerable',
66+
'length',
67+
'caller',
68+
'callee',
69+
'arguments',
70+
'name',
71+
Symbol.toPrimitive,
72+
Symbol.toStringTag,
73+
Symbol.iterator,
74+
];

0 commit comments

Comments
 (0)