// need to remove types: ./src/index before running `yarn build` command /// import produce, { enableES5 } from 'immer' enableES5() import * as React from 'react' import { PureComponent, useLayoutEffect, useEffect, useState, useRef } from 'react' import Global from './global' import { Consumer, consumerActions, get, getInitialState, GlobalContext } from './helper' import { actionMiddlewares, applyMiddlewares, middlewares } from './middlewares' const useStoreEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect const isModelType = (input: any): input is ModelType => { return (input as ModelType).state !== undefined } const isAPI = (input: any): input is API => { return (input as API).useStore !== undefined } // useModel rules: // DON'T USE useModel OUTSIDE createStore func function useModel( state: S | (() => S) ): [S, (state: Partial | ((state: S) => S | void)) => void] { const storeId = Global.currentStoreId const index = Global.mutableState[storeId].count Global.mutableState[storeId].count += 1 if (!Global.mutableState[storeId].hasOwnProperty(index)) { if (typeof state === 'function') { // @ts-ignore Global.mutableState[storeId][index] = state() } else { Global.mutableState[storeId][index] = state } } const setter = async (state: Partial | ((prevState: S) => S | void)) => { if (typeof state === 'function') { Global.mutableState[storeId][index] = produce( Global.mutableState[storeId][index], // @ts-ignore state ) } else { if ( Global.mutableState[storeId][index] && state && Global.mutableState[storeId][index].constructor.name === 'Object' && state.constructor.name === 'Object' ) { Global.mutableState[storeId][index] = { ...Global.mutableState[storeId][index], ...state } } else { Global.mutableState[storeId][index] = state } } const context: InnerContext = { Global, action: () => { return typeof state === 'function' ? // @ts-ignore Global.mutableState[storeId][index] : state }, actionName: 'setter', consumerActions, disableSelectorUpdate: true, middlewareConfig: {}, modelName: storeId, newState: {}, params: undefined, type: 'u' } // pass update event to other components subscribe the same store return await applyMiddlewares(actionMiddlewares, context) } return [Global.mutableState[storeId][index], setter] } function createStore(useHook: CustomModelHook): LaneAPI function createStore(name: string, useHook: CustomModelHook): LaneAPI function createStore(n: any, u?: any): LaneAPI { const hasName = typeof n === 'string' Global.storeId += hasName ? 0 : 1 const storeId = hasName ? n : Global.storeId.toString() if (!Global.Actions[storeId]) { Global.Actions[storeId] = {} } if (!Global.mutableState[storeId]) { Global.mutableState[storeId] = { count: 0 } } // Global.currentStoreId = storeId // const state = useHook() // Global.State = produce(Global.State, (s) => { // s[hash] = state // }) const selector = () => { Global.mutableState[storeId].count = 0 Global.currentStoreId = storeId Global.mutableState[storeId].cachedResult = u ? u() : n() return Global.mutableState[storeId].cachedResult } Global.mutableState[storeId].selector = selector return { // TODO: support selector useStore: () => useStore(storeId, selector), getState: () => selector(), getStore: () => Global.mutableState[storeId].cachedResult, subscribe: (callback: () => void) => { if (!Global.subscriptions[storeId]) { Global.subscriptions[storeId] = [] } Global.subscriptions[storeId].push(callback) }, unsubscribe: (callback?: () => void) => { if (Global.subscriptions[storeId]) { if (callback) { const idx = Global.subscriptions[storeId].indexOf(callback) if (idx >= 0) Global.subscriptions[storeId].splice(idx, 1) } } } } } function Model>( models: MT, // initialState represent extContext here initialState?: E ): API function Model( models: M, initialState?: Global['State'] ): APIs function Model( models: M | MT, initialState?: Global['State'], extContext?: E ) { if (isModelType(models)) { Global.storeId += 1 const hash = '__' + Global.storeId Global.State = produce(Global.State, (s) => { s[hash] = models.state }) if (models.middlewares) { Global.Middlewares[hash] = models.middlewares } Global.Actions[hash] = models.actions Global.AsyncState[hash] = models.asyncState // initialState represent extContext here initialState && (Global.Context[hash] = initialState) const actions = getActions(hash) return { __id: hash, actions, getState: () => getState(hash), subscribe: ( actionName: keyof MT['actions'] | Array, callback: () => void ) => subscribe(hash, actionName as string | string[], callback), unsubscribe: ( actionName: keyof MT['actions'] | Array ) => unsubscribe(hash, actionName as string | string[]), useStore: (selector?: Function) => useStore(hash, selector) } } else { if (initialState) { // TODO: support multi model group under SSR Global.gid = 1 } else { Global.gid += 1 } let prefix = '' if (Global.gid > 1) { prefix = Global.gid + '_' } if (models.actions) { console.error('invalid model(s) schema: ', models) const errorFn = (fakeReturnVal?: unknown) => (..._: unknown[]) => { return fakeReturnVal } // Fallback Functions return { __ERROR__: true, actions: errorFn({}), getActions: errorFn({}), getInitialState: errorFn({}), getState: errorFn({}), subscribe: errorFn(), unsubscribe: errorFn(), useStore: errorFn([{}, {}]) } as any } if (initialState && !initialState.__FROM_SERVER__) { Global.State = produce(Global.State, (s) => { Object.assign(s, initialState || {}) }) } extContext && (Global.Context['__global'] = extContext) let actions: { [name: string]: any } = {} Object.keys(models).forEach((n) => { let name = prefix + n const model = models[n] if (model.__ERROR__) { // Fallback State and Actions when model schema is invalid console.error(name + " model's schema is invalid") Global.State = produce(Global.State, (s) => { s[name] = {} }) Global.Actions[name] = {} return } if (!isAPI(model)) { if (initialState && initialState.__FROM_SERVER__) { Global.State = produce(Global.State, (s) => { s[name] = { ...model.state, ...initialState[name] } }) } else if (!Global.State[name]) { Global.State = produce(Global.State, (s) => { s[name] = model.state }) } if (model.middlewares) { Global.Middlewares[name] = model.middlewares } Global.Actions[name] = model.actions Global.AsyncState[name] = model.asyncState } else { // If you develop on SSR mode, hot reload will still keep the old Global reference, so initialState won't change unless you restart the dev server if (!Global.State[name] || !initialState) { Global.State = produce(Global.State, (s) => { s[name] = s[model.__id] }) } if (initialState && initialState.__FROM_SERVER__) { Global.State = produce(Global.State, (s) => { s[name] = { ...s[model.__id], ...initialState[name] } }) } Global.Actions[name] = Global.Actions[model.__id] Global.AsyncState[name] = Global.AsyncState[model.__id] Global.Middlewares[name] = Global.Middlewares[model.__id] Global.Context[name] = Global.Context[model.__id] } actions[n] = getActions(name) }) return { actions, getActions: (name: string) => getActions(prefix + name), getInitialState: async ( context?: T, config?: { isServer?: boolean } ) => getInitialState(context, { ...config, prefix }), getState: (name: string) => getState(prefix + name), subscribe: ( name: string, actions: keyof MT['actions'] | Array, callback: () => void ) => subscribe(prefix + name, actions as string | string[], callback), unsubscribe: ( name: string, actionName: keyof MT['actions'] | Array ) => unsubscribe(prefix + name, actionName as string | string[]), useStore: (name: string, selector?: Function) => useStore(prefix + name, selector) } as APIs } } const unsubscribe = (modelName: string, actions: string | string[]) => { subscribe(modelName, actions, undefined) } const subscribe = ( modelName: string, actions: string | string[], callback?: () => void ) => { if (Array.isArray(actions)) { actions.forEach((actionName) => { if (!Global.subscriptions[`${modelName}_${actionName}`]) { Global.subscriptions[`${modelName}_${actionName}`] = [] } if (callback) { Global.subscriptions[`${modelName}_${actionName}`].push(callback) } else { Global.subscriptions[`${modelName}_${actionName}`] = [] } }) } else { if (!Global.subscriptions[`${modelName}_${actions}`]) { Global.subscriptions[`${modelName}_${actions}`] = [] } if (callback) { Global.subscriptions[`${modelName}_${actions}`].push(callback) } else { Global.subscriptions[`${modelName}_${actions}`] = [] } } } const getState = (modelName: keyof typeof Global.State) => { return Global.State[modelName] } const getActions = ( modelName: string, baseContext: Partial = { type: 'o' } ) => { const updaters: any = {} Object.keys(Global.Actions[modelName]).forEach( (key) => (updaters[key] = async (params: any, middlewareConfig?: any) => { const context: InnerContext = { action: Global.Actions[modelName][key], actionName: key, consumerActions, middlewareConfig, modelName, newState: null, params, ...baseContext, Global } if (Global.Middlewares[modelName]) { return await applyMiddlewares(Global.Middlewares[modelName], context) } else { return await applyMiddlewares(actionMiddlewares, context) } }) ) return updaters } const useStore = (modelName: string, selector?: Function) => { const setState = useState({})[1] const hash = useRef('') // createStore('xxx', () => {}) has the top priority const mutableState = get([modelName])(Global.mutableState) const isFromCreateStore = !!mutableState const usedSelector = isFromCreateStore ? mutableState.selector : selector const usedState = isFromCreateStore ? mutableState : getState(modelName) useStoreEffect(() => { Global.uid += 1 const local_hash = '' + Global.uid hash.current = local_hash if (!Global.Setter.functionSetter[modelName]) { Global.Setter.functionSetter[modelName] = {} } Global.Setter.functionSetter[modelName][local_hash] = { setState, selector: usedSelector } return function cleanup() { delete Global.Setter.functionSetter[modelName][local_hash] } }, []) if (isFromCreateStore) { return usedSelector(usedState) } else { const updaters = getActions(modelName, { __hash: hash.current, type: 'f' }) return [usedSelector ? usedSelector(usedState) : usedState, updaters] } } // Class API class Provider extends PureComponent<{}, Global['State']> { state = Global.State render() { const { children } = this.props Global.Setter.classSetter = this.setState.bind(this) return ( {children} ) } } const connect = ( modelName: string, mapState?: Function | undefined, mapActions?: Function | undefined ) => (Component: typeof React.Component | typeof PureComponent) => class P extends PureComponent { render() { const { state: prevState = {}, actions: prevActions = {} } = this.props return ( {(models) => { const { [`${modelName}`]: state } = models as any const actions = Global.Actions[modelName] return ( // @ts-ignore ) }} ) } } export { actionMiddlewares, createStore, useModel, Model, middlewares, Provider, Consumer, connect, getState, getInitialState }