, P extends R>(Component: React.ComponentType) => {
107 | return (props: T.Optionalize
): React.ReactElement => {
108 | return (
109 |
110 |
114 |
115 | );
116 | };
117 | };
118 | };
119 |
120 | export default createStore;
121 | export * from './types';
122 |
--------------------------------------------------------------------------------
/src/pluginFactory.ts:
--------------------------------------------------------------------------------
1 | import * as T from './types';
2 | import validate from './utils/validate';
3 |
4 | /**
5 | * PluginFactory
6 | *
7 | * makes Plugin objects extend and inherit from a root PluginFactory
8 | */
9 | export default (config: T.Config) => ({
10 | config,
11 | /**
12 | * validate
13 | *
14 | * bind validate to the store for easy access
15 | */
16 | validate,
17 |
18 | /**
19 | * create plugin
20 | *
21 | * binds plugin properties and functions to an instance of PluginFactorys
22 | * @param plugin
23 | */
24 | create(plugin: T.Plugin): T.Plugin {
25 | validate([
26 | [
27 | plugin.onStoreCreated && typeof plugin.onStoreCreated !== 'function',
28 | 'Plugin onStoreCreated must be a function',
29 | ],
30 | [
31 | plugin.onModel && typeof plugin.onModel !== 'function',
32 | 'Plugin onModel must be a function',
33 | ],
34 | [
35 | plugin.middleware && typeof plugin.middleware !== 'function',
36 | 'Plugin middleware must be a function',
37 | ],
38 | ]);
39 |
40 | if (plugin.onInit) {
41 | plugin.onInit.call(this);
42 | }
43 |
44 | const result: T.Plugin | any = {};
45 |
46 | if (plugin.exposed) {
47 | for (const key of Object.keys(plugin.exposed)) {
48 | this[key] =
49 | typeof plugin.exposed[key] === 'function'
50 | ? plugin.exposed[key].bind(this) // bind functions to plugin class
51 | : Object.create(plugin.exposed[key]); // add exposed to plugin class
52 | }
53 | }
54 | for (const method of ['onModel', 'middleware', 'onStoreCreated']) {
55 | if (plugin[method]) {
56 | result[method] = plugin[method].bind(this);
57 | }
58 | }
59 | return result;
60 | },
61 | });
62 |
--------------------------------------------------------------------------------
/src/plugins/dispatch.ts:
--------------------------------------------------------------------------------
1 | import * as T from '../types';
2 |
3 | /**
4 | * Dispatch Plugin
5 | *
6 | * generates dispatch[modelName][actionName]
7 | */
8 | const dispatchPlugin: T.Plugin = {
9 | exposed: {
10 | // required as a placeholder for store.dispatch
11 | storeDispatch(action: T.Action, state: any) {
12 | console.warn('Warning: store not yet loaded');
13 | },
14 |
15 | storeGetState() {
16 | console.warn('Warning: store not yet loaded');
17 | },
18 |
19 | /**
20 | * dispatch
21 | *
22 | * both a function (dispatch) and an object (dispatch[modelName][actionName])
23 | * @param action T.Action
24 | */
25 | dispatch(action: T.Action) {
26 | return this.storeDispatch(action);
27 | },
28 |
29 | /**
30 | * createDispatcher
31 | *
32 | * genereates an action creator for a given model & reducer
33 | * @param modelName string
34 | * @param reducerName string
35 | */
36 | createDispatcher(modelName: string, reducerName: string) {
37 | return async (payload?: any, meta?: any): Promise => {
38 | const action: T.Action = { type: `${modelName}/${reducerName}` };
39 | if (typeof payload !== 'undefined') {
40 | action.payload = payload;
41 | }
42 | if (typeof meta !== 'undefined') {
43 | action.meta = meta;
44 | }
45 | return this.dispatch(action);
46 | };
47 | },
48 | },
49 |
50 | onStoreCreated(store: any) {
51 | this.storeDispatch = store.dispatch;
52 | this.storeGetState = store.getState;
53 | return { dispatch: this.dispatch };
54 | },
55 |
56 | // generate action creators for all model.reducers
57 | onModel(model: T.Model) {
58 | this.dispatch[model.name] = {};
59 | if (!model.reducers) {
60 | return;
61 | }
62 | for (const reducerName of Object.keys(model.reducers)) {
63 | this.validate([
64 | [
65 | !!reducerName.match(/\/.+\//),
66 | `Invalid reducer name (${model.name}/${reducerName})`,
67 | ],
68 | [
69 | typeof model.reducers[reducerName] !== 'function',
70 | `Invalid reducer (${model.name}/${reducerName}). Must be a function`,
71 | ],
72 | ]);
73 | this.dispatch[model.name][reducerName] = this.createDispatcher.apply(
74 | this,
75 | [model.name, reducerName],
76 | );
77 | }
78 | },
79 | };
80 |
81 | export default dispatchPlugin;
82 |
--------------------------------------------------------------------------------
/src/plugins/effects.ts:
--------------------------------------------------------------------------------
1 | /* tslint-disable member-ordering */
2 | import * as T from '../types';
3 |
4 | /**
5 | * Effects Plugin
6 | *
7 | * Plugin for handling async actions
8 | */
9 | const effectsPlugin: T.Plugin = {
10 | exposed: {
11 | // expose effects for access from dispatch plugin
12 | effects: {},
13 | },
14 |
15 | // add effects to dispatch so that dispatch[modelName][effectName] calls an effect
16 | onModel(model: T.Model): void {
17 | if (!model.effects) {
18 | return;
19 | }
20 |
21 | const effects =
22 | typeof model.effects === 'function'
23 | ? model.effects(this.dispatch)
24 | : model.effects;
25 |
26 | this.validate([
27 | [
28 | typeof effects !== 'object',
29 | `Invalid effects from Model(${model.name}), effects should return an object`,
30 | ],
31 | ]);
32 |
33 | for (const effectName of Object.keys(effects)) {
34 | this.validate([
35 | [
36 | !!effectName.match(/\//),
37 | `Invalid effect name (${model.name}/${effectName})`,
38 | ],
39 | [
40 | typeof effects[effectName] !== 'function',
41 | `Invalid effect (${model.name}/${effectName}). Must be a function`,
42 | ],
43 | ]);
44 |
45 | // provide this.reducer() for effects
46 | this.effects[`${model.name}/${effectName}`] = effects[effectName].bind(
47 | this.dispatch[model.name],
48 | );
49 |
50 | // add effect to dispatch
51 | // is assuming dispatch is available already... that the dispatch plugin is in there
52 | this.dispatch[model.name][effectName] = this.createDispatcher.apply(
53 | this,
54 | [model.name, effectName],
55 | );
56 | // tag effects so they can be differentiated from normal actions
57 | this.dispatch[model.name][effectName].isEffect = true;
58 | }
59 | },
60 |
61 | // process async/await actions
62 | middleware(store) {
63 | return next => async (action: T.Action) => {
64 | // async/await acts as promise middleware
65 | if (action.type in this.effects) {
66 | // effects that share a name with a reducer are called after their reducer counterpart
67 | await next(action);
68 | return this.effects[action.type](
69 | action.payload,
70 | store.getState(),
71 | action.meta,
72 | );
73 | }
74 | return next(action);
75 | };
76 | },
77 | };
78 |
79 | export default effectsPlugin;
80 |
--------------------------------------------------------------------------------
/src/plugins/error.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as T from '../types';
3 |
4 | export interface ErrorConfig {
5 | name?: string;
6 | whitelist?: string[];
7 | blacklist?: string[];
8 | asNumber?: boolean;
9 | }
10 |
11 | interface IErrorState {
12 | error: Error;
13 | value: boolean;
14 | }
15 |
16 | export interface ErrorState {
17 | error: {
18 | global: IErrorState;
19 | models: { [modelName in keyof M]: IErrorState };
20 | effects: {
21 | [modelName in keyof M]: {
22 | [effectName in keyof T.ExtractIModelDispatchersFromEffects<
23 | M[modelName]['effects']
24 | >]: IErrorState
25 | }
26 | };
27 | };
28 | }
29 |
30 | const defaultValue = {
31 | error: null,
32 | value: 0,
33 | };
34 | const cntState = {
35 | global: {
36 | ...defaultValue,
37 | },
38 | models: {},
39 | effects: {},
40 | };
41 | const nextState = {
42 | global: {
43 | ...cntState.global,
44 | },
45 | models: {
46 | ...cntState.models,
47 | },
48 | effects: {
49 | ...cntState.effects,
50 | },
51 | };
52 | function fallback(value) {
53 | return value < 0 ? 0 : value;
54 | }
55 |
56 | const createErrorAction = (converter, i) => (
57 | state,
58 | { name, action }: any,
59 | error: Error,
60 | ) => {
61 | nextState.global = {
62 | value: fallback(nextState.global.value + i),
63 | error,
64 | };
65 | if (typeof nextState.models[name] === 'undefined') {
66 | nextState.models[name] = {
67 | ...defaultValue,
68 | };
69 | }
70 | nextState.models[name] = {
71 | value: fallback(nextState.models[name].value + i),
72 | error,
73 | };
74 | if (typeof nextState.effects[name] === 'undefined') {
75 | nextState.effects[name] = {};
76 | }
77 | if (typeof nextState.effects[name][action] === 'undefined') {
78 | nextState.effects[name][action] = {
79 | ...defaultValue,
80 | };
81 | }
82 | nextState.effects[name][action] = {
83 | value: fallback(nextState.effects[name][action].value + i),
84 | error,
85 | };
86 |
87 | return {
88 | ...state,
89 | global: converter(nextState.global),
90 | models: {
91 | ...state.models,
92 | [name]: converter(nextState.models[name]),
93 | },
94 | effects: {
95 | ...state.effects,
96 | [name]: {
97 | ...state.effects[name],
98 | [action]: converter(nextState.effects[name][action]),
99 | },
100 | },
101 | };
102 | };
103 |
104 | const validateConfig = config => {
105 | if (config.name && typeof config.name !== 'string') {
106 | throw new Error('error plugin config name must be a string');
107 | }
108 | if (config.asNumber && typeof config.asNumber !== 'boolean') {
109 | throw new Error('error plugin config asNumber must be a boolean');
110 | }
111 | if (config.whitelist && !Array.isArray(config.whitelist)) {
112 | throw new Error(
113 | 'error plugin config whitelist must be an array of strings',
114 | );
115 | }
116 | if (config.blacklist && !Array.isArray(config.blacklist)) {
117 | throw new Error(
118 | 'error plugin config blacklist must be an array of strings',
119 | );
120 | }
121 | if (config.whitelist && config.blacklist) {
122 | throw new Error(
123 | 'error plugin config cannot have both a whitelist & a blacklist',
124 | );
125 | }
126 | };
127 |
128 | export default (config: ErrorConfig = {}): T.Plugin => {
129 | validateConfig(config);
130 |
131 | const errorModelName = config.name || 'error';
132 |
133 | const converter =
134 | config.asNumber === true
135 | ? cnt => cnt
136 | : cnt => ({
137 | ...cnt,
138 | value: cnt.value > 0,
139 | });
140 |
141 | const error: T.Model = {
142 | name: errorModelName,
143 | reducers: {
144 | hide: createErrorAction(converter, -1),
145 | show: createErrorAction(converter, 1),
146 | },
147 | state: {
148 | ...cntState,
149 | },
150 | };
151 |
152 | cntState.global = {
153 | ...defaultValue,
154 | };
155 | error.state.global = converter(cntState.global);
156 |
157 | return {
158 | config: {
159 | models: {
160 | error,
161 | },
162 | },
163 | onModel({ name }: T.Model) {
164 | // do not run dispatch on 'error' model
165 | if (name === errorModelName) {
166 | return;
167 | }
168 |
169 | cntState.models[name] = {
170 | ...defaultValue,
171 | };
172 | error.state.models[name] = converter(cntState.models[name]);
173 | error.state.effects[name] = {};
174 | const modelActions = this.dispatch[name];
175 |
176 | // map over effects within models
177 | Object.keys(modelActions).forEach((action: string) => {
178 | if (this.dispatch[name][action].isEffect !== true) {
179 | return;
180 | }
181 |
182 | cntState.effects[name][action] = {
183 | ...defaultValue,
184 | };
185 | error.state.effects[name][action] = converter(
186 | cntState.effects[name][action],
187 | );
188 |
189 | const actionType = `${name}/${action}`;
190 |
191 | // ignore items not in whitelist
192 | if (config.whitelist && !config.whitelist.includes(actionType)) {
193 | return;
194 | }
195 |
196 | // ignore items in blacklist
197 | if (config.blacklist && config.blacklist.includes(actionType)) {
198 | return;
199 | }
200 |
201 | // copy orig effect pointer
202 | const origEffect = this.dispatch[name][action];
203 |
204 | // create function with pre & post error calls
205 | const effectWrapper = async (...props) => {
206 | // only clear when there has been a error
207 | if (nextState.effects[name] && nextState.effects[name][action] && nextState.effects[name][action].error) {
208 | this.dispatch.error.hide({ name, action }, null);
209 | }
210 | try {
211 | return await origEffect(...props);
212 | } catch (error) {
213 | // display error on console
214 | console.error(error);
215 | this.dispatch.error.show({ name, action }, error);
216 | }
217 | };
218 |
219 | effectWrapper.isEffect = true;
220 |
221 | // replace existing effect with new wrapper
222 | this.dispatch[name][action] = effectWrapper;
223 | });
224 | },
225 | };
226 | };
227 |
--------------------------------------------------------------------------------
/src/plugins/immer.ts:
--------------------------------------------------------------------------------
1 | import produce, { enableES5 } from 'immer';
2 | import { combineReducers, ReducersMapObject } from 'redux';
3 | import * as T from '../types';
4 |
5 | // make it work in IE11
6 | enableES5();
7 |
8 | export interface ImmerConfig {
9 | blacklist?: string[];
10 | }
11 |
12 | function createCombineReducersWithImmer(blacklist: string[] = []) {
13 | return function (reducers: ReducersMapObject) {
14 | const reducersWithImmer: ReducersMapObject> = {};
15 | // reducer must return value because literal don't support immer
16 |
17 | Object.keys(reducers).forEach((key) => {
18 | const reducerFn = reducers[key];
19 | reducersWithImmer[key] = (state, payload) =>
20 | (typeof state === 'object' && !blacklist.includes(key)
21 | ? produce(state, (draft: T.Models) => {
22 | const next = reducerFn(draft, payload);
23 | if (typeof next === 'object') return next;
24 | })
25 | : reducerFn(state, payload));
26 | });
27 |
28 | return combineReducers(reducersWithImmer);
29 | };
30 | }
31 |
32 | // icestore plugin
33 | const immerPlugin = (config: ImmerConfig = {}): T.Plugin => ({
34 | config: {
35 | redux: {
36 | combineReducers: createCombineReducersWithImmer(config.blacklist),
37 | },
38 | },
39 | });
40 |
41 | export default immerPlugin;
42 |
--------------------------------------------------------------------------------
/src/plugins/loading.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as T from '../types';
3 |
4 | export interface LoadingConfig {
5 | name?: string;
6 | whitelist?: string[];
7 | blacklist?: string[];
8 | asNumber?: boolean;
9 | }
10 |
11 | export interface LoadingState {
12 | loading: {
13 | global: boolean;
14 | models: { [modelName in keyof M]: boolean };
15 | effects: {
16 | [modelName in keyof M]: {
17 | [effectName in keyof T.ExtractIModelDispatchersFromEffects]: boolean
18 | }
19 | };
20 | };
21 | }
22 |
23 | const cntState = {
24 | global: 0,
25 | models: {},
26 | effects: {},
27 | };
28 | const nextState = {
29 | ...cntState,
30 | models: {
31 | ...cntState.models,
32 | },
33 | effects: {
34 | ...cntState.effects,
35 | },
36 | };
37 |
38 | const createLoadingAction = (converter, i) => (
39 | state,
40 | { name, action }: any,
41 | ) => {
42 | nextState.global += i;
43 | if (typeof nextState.models[name] === 'undefined') {
44 | nextState.models[name] = 0;
45 | }
46 | nextState.models[name] += i;
47 |
48 | if (typeof nextState.effects[name] === 'undefined') {
49 | nextState.effects[name] = {};
50 | }
51 | if (typeof nextState.effects[name][action] === 'undefined') {
52 | nextState.effects[name][action] = 0;
53 | }
54 | nextState.effects[name][action] += i;
55 |
56 | return {
57 | ...state,
58 | global: converter(nextState.global),
59 | models: {
60 | ...state.models,
61 | [name]: converter(nextState.models[name]),
62 | },
63 | effects: {
64 | ...state.effects,
65 | [name]: {
66 | ...state.effects[name],
67 | [action]: converter(nextState.effects[name][action]),
68 | },
69 | },
70 | };
71 | };
72 |
73 | const validateConfig = config => {
74 | if (config.name && typeof config.name !== 'string') {
75 | throw new Error('loading plugin config name must be a string');
76 | }
77 | if (config.asNumber && typeof config.asNumber !== 'boolean') {
78 | throw new Error('loading plugin config asNumber must be a boolean');
79 | }
80 | if (config.whitelist && !Array.isArray(config.whitelist)) {
81 | throw new Error(
82 | 'loading plugin config whitelist must be an array of strings',
83 | );
84 | }
85 | if (config.blacklist && !Array.isArray(config.blacklist)) {
86 | throw new Error(
87 | 'loading plugin config blacklist must be an array of strings',
88 | );
89 | }
90 | if (config.whitelist && config.blacklist) {
91 | throw new Error(
92 | 'loading plugin config cannot have both a whitelist & a blacklist',
93 | );
94 | }
95 | };
96 |
97 | export default (config: LoadingConfig = {}): T.Plugin => {
98 | validateConfig(config);
99 |
100 | const loadingModelName = config.name || 'loading';
101 |
102 | const converter =
103 | config.asNumber === true ? (cnt: number) => cnt : (cnt: number) => cnt > 0;
104 |
105 | const loading: T.Model = {
106 | name: loadingModelName,
107 | reducers: {
108 | hide: createLoadingAction(converter, -1),
109 | show: createLoadingAction(converter, 1),
110 | },
111 | state: {
112 | ...cntState,
113 | },
114 | };
115 |
116 | cntState.global = 0;
117 | loading.state.global = converter(cntState.global);
118 |
119 | return {
120 | config: {
121 | models: {
122 | loading,
123 | },
124 | },
125 | onModel({ name }: T.Model) {
126 | // do not run dispatch on 'loading' model
127 | if (name === loadingModelName) {
128 | return;
129 | }
130 |
131 | cntState.models[name] = 0;
132 | loading.state.models[name] = converter(cntState.models[name]);
133 | loading.state.effects[name] = {};
134 | const modelActions = this.dispatch[name];
135 |
136 | // map over effects within models
137 | Object.keys(modelActions).forEach((action: string) => {
138 | if (this.dispatch[name][action].isEffect !== true) {
139 | return;
140 | }
141 |
142 | cntState.effects[name][action] = 0;
143 | loading.state.effects[name][action] = converter(
144 | cntState.effects[name][action],
145 | );
146 |
147 | const actionType = `${name}/${action}`;
148 |
149 | // ignore items not in whitelist
150 | if (config.whitelist && !config.whitelist.includes(actionType)) {
151 | return;
152 | }
153 |
154 | // ignore items in blacklist
155 | if (config.blacklist && config.blacklist.includes(actionType)) {
156 | return;
157 | }
158 |
159 | // copy orig effect pointer
160 | const origEffect = this.dispatch[name][action];
161 |
162 | // create function with pre & post loading calls
163 | const effectWrapper = async (...props) => {
164 | try {
165 | this.dispatch.loading.show({ name, action });
166 | // waits for dispatch function to finish before calling 'hide'
167 | const effectResult = await origEffect(...props);
168 | this.dispatch.loading.hide({ name, action });
169 | return effectResult;
170 | } catch (error) {
171 | this.dispatch.loading.hide({ name, action });
172 | throw error;
173 | }
174 | };
175 |
176 | effectWrapper.isEffect = true;
177 |
178 | // replace existing effect with new wrapper
179 | this.dispatch[name][action] = effectWrapper;
180 | });
181 | },
182 | };
183 | };
184 |
--------------------------------------------------------------------------------
/src/plugins/modelApis.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as T from '../types';
3 |
4 | /**
5 | * ModelApis Plugin
6 | *
7 | * generates hooks for store
8 | */
9 | export default (): T.Plugin => {
10 | return {
11 | onStoreCreated(store: any) {
12 | // hooks
13 | function useModel(name) {
14 | const state = useModelState(name);
15 | const dispatchers = useModelDispatchers(name);
16 | return [state, dispatchers];
17 | }
18 | function useModelState(name) {
19 | const selector = store.useSelector(state => state[name]);
20 | if (typeof selector !== 'undefined') {
21 | return selector;
22 | }
23 | throw new Error(`Not found model by namespace: ${name}.`);
24 | }
25 | function useModelDispatchers(name) {
26 | const dispatch = store.useDispatch();
27 | if (dispatch[name]) {
28 | return dispatch[name];
29 | }
30 | throw new Error(`Not found model by namespace: ${name}.`);
31 | }
32 | function useModelEffectsState(name) {
33 | const dispatch = useModelDispatchers(name);
34 | const effectsLoading = useModelEffectsLoading(name);
35 | const effectsError = useModelEffectsError(name);
36 |
37 | const states = {};
38 | Object.keys(dispatch).forEach(key => {
39 | states[key] = {
40 | isLoading: effectsLoading[key],
41 | error: effectsError[key] ? effectsError[key].error : null,
42 | };
43 | });
44 | return states;
45 | }
46 | function useModelEffectsError(name) {
47 | return store.useSelector(state => (state.error ? state.error.effects[name] : undefined));
48 | }
49 | function useModelEffectsLoading(name) {
50 | return store.useSelector(state => (state.loading ? state.loading.effects[name] : undefined));
51 | }
52 |
53 | // other apis
54 | function getModel(name) {
55 | return [getModelState(name), getModelDispatchers(name)];
56 | }
57 | function getModelState(name) {
58 | return store.getState()[name];
59 | }
60 | function getModelDispatchers(name) {
61 | return store.dispatch[name];
62 | }
63 |
64 | // class component support
65 | function withModel(name, mapModelToProps?) {
66 | mapModelToProps = (mapModelToProps || ((model) => ({ [name]: model })));
67 | return (Component) => {
68 | return (props): React.ReactElement => {
69 | const value = useModel(name);
70 | const withProps = mapModelToProps(value);
71 | return (
72 |
76 | );
77 | };
78 | };
79 | }
80 |
81 |
82 | function createWithModelDispatchers(fieldSuffix = 'Dispatchers') {
83 | return function withModelDispatchers(name, mapModelDispatchersToProps?) {
84 | mapModelDispatchersToProps = (mapModelDispatchersToProps || ((dispatch) => ({ [`${name}${fieldSuffix}`]: dispatch })));
85 | return (Component) => {
86 | return (props): React.ReactElement => {
87 | const dispatchers = useModelDispatchers(name);
88 | const withProps = mapModelDispatchersToProps(dispatchers);
89 | return (
90 |
94 | );
95 | };
96 | };
97 | };
98 | }
99 | const withModelDispatchers = createWithModelDispatchers();
100 |
101 | function createWithModelEffectsState(fieldSuffix = 'EffectsState') {
102 | return function (name, mapModelEffectsStateToProps?) {
103 | mapModelEffectsStateToProps = (mapModelEffectsStateToProps || ((effectsState) => ({ [`${name}${fieldSuffix}`]: effectsState })));
104 | return (Component) => {
105 | return (props): React.ReactElement => {
106 | const value = useModelEffectsState(name);
107 | const withProps = mapModelEffectsStateToProps(value);
108 | return (
109 |
113 | );
114 | };
115 | };
116 | };
117 | }
118 | const withModelEffectsState = createWithModelEffectsState();
119 |
120 | function withModelEffectsError(name, mapModelEffectsErrorToProps?) {
121 | mapModelEffectsErrorToProps = (mapModelEffectsErrorToProps || ((errors) => ({ [`${name}EffectsError`]: errors })));
122 | return (Component) => {
123 | return (props): React.ReactElement => {
124 | const value = useModelEffectsError(name);
125 | const withProps = mapModelEffectsErrorToProps(value);
126 | return (
127 |
131 | );
132 | };
133 | };
134 | }
135 |
136 | function withModelEffectsLoading(name?, mapModelEffectsLoadingToProps?) {
137 | mapModelEffectsLoadingToProps = (mapModelEffectsLoadingToProps || ((loadings) => ({ [`${name}EffectsLoading`]: loadings })));
138 | return (Component) => {
139 | return (props): React.ReactElement => {
140 | const value = useModelEffectsLoading(name);
141 | const withProps = mapModelEffectsLoadingToProps(value);
142 | return (
143 |
147 | );
148 | };
149 | };
150 | }
151 |
152 | function getModelAPIs(name) {
153 | return {
154 | useValue: () => useModel(name),
155 | useState: () => useModelState(name),
156 | useDispatchers: () => useModelDispatchers(name),
157 | useEffectsState: () => useModelEffectsState(name),
158 | useEffectsError: () => useModelEffectsError(name),
159 | useEffectsLoading: () => useModelEffectsLoading(name),
160 | getValue: () => getModel(name),
161 | getState: () => getModelState(name),
162 | getDispatchers: () => getModelDispatchers(name),
163 | withValue: (mapToProps?) => withModel(name, mapToProps),
164 | withDispatchers: (mapToProps?) => withModelDispatchers(name, mapToProps),
165 | withEffectsState: (mapToProps?) => withModelEffectsState(name, mapToProps),
166 | withEffectsError: (mapToProps?) => withModelEffectsError(name, mapToProps),
167 | withEffectsLoading: (mapToProps?) => withModelEffectsLoading(name, mapToProps),
168 | };
169 | }
170 |
171 | return {
172 | getModelAPIs,
173 |
174 | // Hooks
175 | useModel,
176 | useModelState,
177 | useModelDispatchers,
178 | useModelEffectsState,
179 | useModelEffectsError,
180 | useModelEffectsLoading,
181 |
182 | // real time
183 | getModel,
184 | getModelState,
185 | getModelDispatchers,
186 |
187 | // Class component support
188 | withModel,
189 | withModelDispatchers,
190 | withModelEffectsState,
191 | withModelEffectsError,
192 | withModelEffectsLoading,
193 | };
194 | },
195 | };
196 | };
197 |
--------------------------------------------------------------------------------
/src/plugins/provider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider as ReduxProvider } from 'react-redux';
3 | import * as T from '../types';
4 | import actionTypes from '../actionTypes';
5 |
6 | const { SET_STATE } = actionTypes;
7 |
8 | interface ProviderConfig {
9 | context: React.Context;
10 | }
11 |
12 | export default ({ context }: ProviderConfig): T.Plugin => {
13 | return {
14 | onStoreCreated(store: any) {
15 | const Provider = function (props) {
16 | const { children, initialStates } = props;
17 | if (initialStates) {
18 | Object.keys(initialStates).forEach(name => {
19 | const initialState = initialStates[name];
20 | if (initialState && store.dispatch[name][SET_STATE]) {
21 | store.dispatch[name][SET_STATE](initialState);
22 | }
23 | });
24 | }
25 | return (
26 | // @ts-ignore
27 |
28 | {children}
29 |
30 | );
31 | };
32 | return { Provider, context };
33 | },
34 | };
35 | };
36 |
37 |
--------------------------------------------------------------------------------
/src/plugins/reduxHooks.ts:
--------------------------------------------------------------------------------
1 | import { createSelectorHook, createDispatchHook } from 'react-redux';
2 | import * as T from '../types';
3 |
4 | interface ReduxHooksConfig {
5 | context: any;
6 | }
7 |
8 | /**
9 | * Redux Hooks Plugin
10 | *
11 | * generates redux hooks for store
12 | */
13 | export default ({ context }: ReduxHooksConfig): T.Plugin => {
14 | const useSelector = createSelectorHook(context);
15 | const useDispatch = createDispatchHook(context);
16 |
17 | return {
18 | onStoreCreated() {
19 | return {
20 | useSelector,
21 | useDispatch,
22 | };
23 | },
24 | };
25 | };
26 |
27 |
--------------------------------------------------------------------------------
/src/redux.ts:
--------------------------------------------------------------------------------
1 | import * as Redux from 'redux';
2 | import * as T from './types';
3 | import isListener from './utils/isListener';
4 |
5 | const composeEnhancersWithDevtools = (
6 | devtoolOptions: T.DevtoolOptions = {},
7 | ): any => {
8 | const { disabled, ...options } = devtoolOptions;
9 | /* istanbul ignore next */
10 | return !disabled &&
11 | typeof window === 'object' &&
12 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
13 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__(options)
14 | : Redux.compose;
15 | };
16 |
17 | export default function ({
18 | redux,
19 | models,
20 | }: {
21 | redux: T.ConfigRedux;
22 | models: T.Model[];
23 | }) {
24 | const combineReducers = redux.combineReducers || Redux.combineReducers;
25 | const createStore: Redux.StoreCreator = redux.createStore || Redux.createStore;
26 | const initialStates: any =
27 | typeof redux.initialStates !== 'undefined' ? redux.initialStates : {};
28 |
29 | // Allows passing in of reducer functions, rather than models.
30 | // While not recommended,
31 | // this can be used for migrating a Redux codebase or configuring different Redux extensions.
32 | this.reducers = redux.reducers;
33 |
34 | // combine models to generate reducers
35 | this.mergeReducers = (nextReducers: T.ModelReducers = {}) => {
36 | // merge new reducers with existing reducers
37 | this.reducers = { ...this.reducers, ...nextReducers };
38 | if (!Object.keys(this.reducers).length) {
39 | // no reducers, just return state
40 | return (state: any) => state;
41 | }
42 | return combineReducers(this.reducers);
43 | };
44 |
45 | this.createModelReducer = (model: T.Model) => {
46 | const modelBaseReducer = model.baseReducer;
47 | const modelReducers = {};
48 | for (const modelReducer of Object.keys(model.reducers || {})) {
49 | const action = isListener(modelReducer)
50 | ? modelReducer
51 | : `${model.name}/${modelReducer}`;
52 | modelReducers[action] = model.reducers[modelReducer];
53 | }
54 |
55 | // use the `state = model.state` argument convention popularized
56 | const combinedReducer = (state: any = model.state, action: T.Action) => {
57 | if (typeof modelReducers[action.type] === 'function') {
58 | return modelReducers[action.type](state, action.payload, action.meta);
59 | }
60 | return state;
61 | };
62 |
63 | this.reducers[model.name] = !modelBaseReducer
64 | ? combinedReducer
65 | : (state: any, action: T.Action) =>
66 | combinedReducer(modelBaseReducer(state, action), action);
67 | };
68 |
69 | // initialize model reducers
70 | for (const model of models) {
71 | this.createModelReducer(model);
72 | }
73 |
74 | // rootReducers is a way to setup middleware hooks at the base of your root reducer.
75 | // Unlike middleware, the return value is the next state.
76 | // If undefined, the state will fallback to the initial state of reducers.
77 | this.createRootReducer = (
78 | rootReducers: T.RootReducers = {},
79 | ): Redux.Reducer => {
80 | const mergedReducers: Redux.Reducer = this.mergeReducers();
81 | if (Object.keys(rootReducers).length) {
82 | return (state, action) => {
83 | const rootReducerAction = rootReducers[action.type];
84 | if (rootReducerAction) {
85 | return mergedReducers(rootReducerAction(state, action), action);
86 | }
87 | return mergedReducers(state, action);
88 | };
89 | }
90 | return mergedReducers;
91 | };
92 |
93 | const rootReducer = this.createRootReducer(redux.rootReducers);
94 |
95 | const middlewares = Redux.applyMiddleware(...redux.middlewares);
96 | const enhancers = composeEnhancersWithDevtools(redux.devtoolOptions)(
97 | ...redux.enhancers,
98 | middlewares,
99 | );
100 |
101 | this.store = createStore(rootReducer, initialStates, enhancers);
102 |
103 | return this;
104 | }
105 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import * as Redux from 'redux';
2 | import React from 'react';
3 |
4 | export type Optionalize = Omit;
5 |
6 | type PropType = Obj[Prop];
7 |
8 | interface EffectState {
9 | isLoading: boolean;
10 | error: Error;
11 | }
12 |
13 | type EffectsState = {
14 | [K in keyof Effects]: EffectState;
15 | };
16 |
17 | type EffectsLoading = {
18 | [K in keyof Effects]: boolean;
19 | };
20 |
21 | type EffectsError = {
22 | [K in keyof Effects]: {
23 | error: Error;
24 | value: number;
25 | };
26 | };
27 |
28 | export type ExtractIcestoreStateFromModels = {
29 | [modelKey in keyof M]: M[modelKey]['state']
30 | };
31 |
32 | // should declare by user
33 | // eslint-disable-next-line @typescript-eslint/no-empty-interface
34 | export interface IcestoreModels extends Models {}
35 |
36 | export type IcestoreRootState = ExtractIcestoreStateFromModels<
37 | M
38 | >;
39 |
40 | type ExtractIModelDispatcherAsyncFromEffect<
41 | E,
42 | > = E extends () => Promise
43 | ? IcestoreDispatcherAsync
44 | : E extends (payload: infer P) => Promise
45 | ? IcestoreDispatcherAsync
46 | : E extends (payload: infer P, rootState: any) => Promise
47 | ? IcestoreDispatcherAsync
48 | : E extends (payload: infer P, rootState: any, meta: infer M) => Promise
49 | ? IcestoreDispatcherAsync
50 | : IcestoreDispatcherAsync;
51 |
52 | type ExtractIModelDispatchersFromEffectsObject<
53 | effects extends ModelEffects,
54 | > = {
55 | [effectKey in keyof effects]: ExtractIModelDispatcherAsyncFromEffect<
56 | effects[effectKey]
57 | >
58 | };
59 |
60 | export type ExtractIModelDispatchersFromEffects<
61 | effects extends ModelConfig['effects'],
62 | > = effects extends ((...args: any[]) => infer R)
63 | ? R extends ModelEffects
64 | ? ExtractIModelDispatchersFromEffectsObject
65 | : {}
66 | : effects extends ModelEffects
67 | ? ExtractIModelDispatchersFromEffectsObject
68 | : {};
69 |
70 | type ExtractIModelDispatcherFromReducer = R extends () => any
71 | ? IcestoreDispatcher
72 | : R extends (state: infer S) => infer S
73 | ? IcestoreDispatcher
74 | : R extends (state: infer S, payload: infer P) => (infer S | void)
75 | ? IcestoreDispatcher
76 | : R extends (state: infer S, payload: infer P, meta: infer M) => (infer S | void)
77 | ? IcestoreDispatcher
78 | : IcestoreDispatcher;
79 |
80 | interface DefaultIModelDispatchersFromReducersObject {
81 | setState: IcestoreDispatcher;
82 | }
83 |
84 | type ExtractIModelDispatchersFromReducersObject<
85 | reducers extends ModelReducers,
86 | > = {
87 | [reducerKey in keyof reducers]: ExtractIModelDispatcherFromReducer<
88 | reducers[reducerKey]
89 | >;
90 | } & DefaultIModelDispatchersFromReducersObject;
91 |
92 | export type ExtractIModelDispatchersFromReducers<
93 | reducers extends ModelConfig['reducers'],
94 | > = ExtractIModelDispatchersFromReducersObject;
95 |
96 | export type ExtractIModelStateFromModelConfig = PropType;
97 |
98 | export type ExtractIModelEffectsFromModelConfig = PropType;
99 |
100 | export type ExtractIModelReducersFromModelConfig = PropType;
101 |
102 | export type ExtractIModelFromModelConfig = [
103 | ExtractIModelStateFromModelConfig,
104 | ExtractIModelDispatchersFromModelConfig,
105 | ];
106 |
107 | export type ExtractIModelEffectsErrorFromModelConfig = EffectsError<
108 | ExtractIModelDispatchersFromEffects>
109 | >;
110 |
111 | export type ExtractIModelEffectsLoadingFromModelConfig = EffectsLoading<
112 | ExtractIModelDispatchersFromEffects>
113 | >;
114 |
115 | export type ExtractIModelEffectsStateFromModelConfig = EffectsState<
116 | ExtractIModelDispatchersFromEffects>
117 | >;
118 |
119 | export type ExtractIModelDispatchersFromModelConfig<
120 | M extends ModelConfig,
121 | > = ExtractIModelDispatchersFromReducers> &
122 | ExtractIModelDispatchersFromEffects>;
123 |
124 | export type ExtractIcestoreDispatchersFromModels = {
125 | [modelKey in keyof M]: ExtractIModelDispatchersFromModelConfig
126 | };
127 |
128 | type IcestoreDispatcher = ([P] extends [void]
129 | ? ((...args: any[]) => Action)
130 | : [M] extends [void]
131 | ? ((payload: P) => Action)
132 | : (payload: P, meta: M) => Action
) &
133 | ((action: Action
) => Redux.Dispatch>) &
134 | ((action: Action) => Redux.Dispatch>);
135 |
136 | type IcestoreDispatcherAsync = ([P] extends [void]
137 | ? ((...args: any[]) => Promise)
138 | : [M] extends [void]
139 | ? ((payload: P) => Promise)
140 | : (payload: P, meta: M) => Promise) &
141 | ((action: Action) => Promise) &
142 | ((action: Action) => Promise);
143 |
144 | export type IcestoreDispatch = (M extends Models
145 | ? ExtractIcestoreDispatchersFromModels
146 | : {
147 | [key: string]: {
148 | [key: string]: IcestoreDispatcher | IcestoreDispatcherAsync;
149 | };
150 | }) &
151 | (IcestoreDispatcher | IcestoreDispatcherAsync) &
152 | (Redux.Dispatch); // for library compatability
153 |
154 | export interface Icestore<
155 | M extends Models = Models,
156 | A extends Action = Action,
157 | > extends Redux.Store, A> {
158 | name: string;
159 | replaceReducer: (nextReducer: Redux.Reducer, A>) => void;
160 | dispatch: IcestoreDispatch;
161 | getState: () => IcestoreRootState;
162 | model: (model: Model) => void;
163 | subscribe: (listener: () => void) => Redux.Unsubscribe;
164 | }
165 |
166 | interface UseModelEffectsError {
167 | (name: K): ExtractIModelEffectsErrorFromModelConfig;
168 | }
169 |
170 | interface MapModelEffectsErrorToProps {
171 | (effectsLoading: ExtractIModelEffectsErrorFromModelConfig): Record;
172 | }
173 |
174 | interface WithModelEffectsError = MapModelEffectsErrorToProps> {
175 | (name: K, mapModelEffectsErrorToProps?: F):
176 | , P extends R>(Component: React.ComponentType) =>
177 | (props: Optionalize
) => React.ReactElement;
178 | }
179 |
180 | interface ModelEffectsErrorAPI {
181 | useModelEffectsError: UseModelEffectsError;
182 | withModelEffectsError: WithModelEffectsError;
183 | }
184 |
185 | interface UseModelEffectsLoading {
186 | (name: K): ExtractIModelEffectsLoadingFromModelConfig;
187 | }
188 |
189 | interface MapModelEffectsLoadingToProps {
190 | (effectsLoading: ExtractIModelEffectsLoadingFromModelConfig): Record;
191 | }
192 |
193 | interface WithModelEffectsLoading = MapModelEffectsLoadingToProps> {
194 | (name: K, mapModelEffectsLoadingToProps?: F):
195 | , P extends R>(Component: React.ComponentType) =>
196 | (props: Optionalize
) => React.ReactElement;
197 | }
198 |
199 | interface ModelEffectsLoadingAPI {
200 | useModelEffectsLoading: UseModelEffectsLoading;
201 | withModelEffectsLoading: WithModelEffectsLoading;
202 | }
203 |
204 | interface UseModelEffectsState {
205 | (name: K): ExtractIModelEffectsStateFromModelConfig;
206 | }
207 |
208 | interface MapModelEffectsStateToProps {
209 | (effectsState: ExtractIModelEffectsStateFromModelConfig): Record;
210 | }
211 |
212 | interface WithModelEffectsState = MapModelEffectsStateToProps> {
213 | (name: K, mapModelEffectsStateToProps?: F):
214 | , P extends R>(Component: React.ComponentType) =>
215 | (props: Optionalize
) => React.ReactElement;
216 | }
217 |
218 | interface ModelEffectsStateAPI {
219 | useModelEffectsState: UseModelEffectsState;
220 | withModelEffectsState: WithModelEffectsState;
221 | }
222 |
223 | interface UseModelState {
224 | (name: K): ExtractIModelStateFromModelConfig;
225 | }
226 |
227 | interface ModelStateAPI {
228 | useModelState: UseModelState;
229 | getModelState: UseModelState;
230 | }
231 |
232 | interface UseModelDispatchers {
233 | (name: K): ExtractIModelDispatchersFromModelConfig;
234 | }
235 |
236 | interface MapModelDispatchersToProps {
237 | (dispatchers: ExtractIModelDispatchersFromModelConfig): Record;
238 | }
239 |
240 | interface WithModelDispatchers = MapModelDispatchersToProps> {
241 | (name: K, mapModelDispatchersToProps?: F):
242 | , P extends R>(Component: React.ComponentType) =>
243 | (props: Optionalize
) => React.ReactElement;
244 | }
245 |
246 | interface ModelDispathersAPI {
247 | useModelDispatchers: UseModelDispatchers;
248 | getModelDispatchers: UseModelDispatchers;
249 | withModelDispatchers: WithModelDispatchers;
250 | }
251 |
252 | interface UseModel {
253 | (name: K): ExtractIModelFromModelConfig;
254 | }
255 |
256 | interface MapModelToProps {
257 | (model: ExtractIModelFromModelConfig): Record;
258 | }
259 |
260 | interface WithModel = MapModelToProps> {
261 | (name: K, mapModelToProps?: F):
262 | , P extends R>(Component: React.ComponentType) =>
263 | (props: Optionalize
) => React.ReactElement;
264 | }
265 |
266 | interface ModelValueAPI {
267 | useModel: UseModel;
268 | getModel: UseModel;
269 | withModel: WithModel;
270 | }
271 |
272 | interface GetModelAPIsValue {
273 | // ModelValueAPI
274 | useValue: () => ReturnType>;
275 | getValue: () => ReturnType>;
276 | withValue: >(f?: F) => ReturnType>;
277 | // ModelStateAPI
278 | useModelState: () => ReturnType>;
279 | getModelState: () => ReturnType>;
280 | // ModelDispathersAPI
281 | useDispatchers: () => ReturnType>;
282 | getDispatchers: () => ReturnType>;
283 | withDispatchers: >(f?: F) => ReturnType>;
284 | // ModelEffectsLoadingAPI
285 | useEffectsLoading: () => ReturnType>;
286 | withEffectsLoading: >(f?: F) => ReturnType>;
287 | // ModelEffectsErrorAPI
288 | useEffectsError: () => ReturnType>;
289 | withEffectsError: >(f?: F) => ReturnType>;
290 | // ModelEffectsStateAPI
291 | useModelEffectsState: () => ReturnType>;
292 | withModelEffectsState: >(f?: F) => ReturnType>;
293 | }
294 |
295 | interface GetModelAPIs {
296 | (name: K): GetModelAPIsValue;
297 | }
298 |
299 | type ModelAPI =
300 | {
301 | getModelAPIs: GetModelAPIs;
302 | } &
303 | ModelValueAPI &
304 | ModelStateAPI &
305 | ModelDispathersAPI &
306 | ModelEffectsLoadingAPI &
307 | ModelEffectsErrorAPI &
308 | ModelEffectsStateAPI;
309 |
310 | interface ProviderProps {
311 | children: any;
312 | initialStates?: any;
313 | }
314 |
315 | interface ProviderPluginAPI {
316 | Provider: (props: ProviderProps) => JSX.Element;
317 | context: React.Context<{ store: PresetIcestore }>;
318 | }
319 |
320 | export type ExtractIModelAPIsFromModelConfig = ReturnType>;
321 |
322 | export type PresetIcestore<
323 | M extends Models = Models,
324 | A extends Action = Action,
325 | > = Icestore &
326 | ModelAPI &
327 | ProviderPluginAPI;
328 |
329 | export interface Action {
330 | type: string;
331 | payload?: P;
332 | meta?: M;
333 | }
334 |
335 | export interface ModelReducers {
336 | [key: string]: (state: S, payload: any, meta?: any) => S | void;
337 | }
338 |
339 | export interface ModelEffects {
340 | [key: string]: (
341 | payload: any,
342 | rootState?: S,
343 | meta?: any
344 | ) => void;
345 | }
346 |
347 | export interface Models {
348 | [key: string]: ModelConfig;
349 | }
350 |
351 | export type ModelHook = (model: Model) => void;
352 |
353 | export type Validation = [boolean | undefined, string];
354 |
355 | export interface Model extends ModelConfig {
356 | name: string;
357 | reducers: ModelReducers;
358 | }
359 |
360 | export interface ModelConfig {
361 | name?: string;
362 | state: S;
363 | baseReducer?: (state: SS, action: Action) => SS;
364 | reducers?: ModelReducers;
365 | effects?:
366 | | ModelEffects
367 | | ((dispatch: IcestoreDispatch) => ModelEffects);
368 | }
369 |
370 | export interface PluginFactory extends Plugin {
371 | create: (plugin: Plugin) => Plugin;
372 | }
373 |
374 | export interface Plugin