diff --git a/src/.umi/core/routes.ts b/src/.umi/core/routes.ts index 1a17502..2803f75 100644 --- a/src/.umi/core/routes.ts +++ b/src/.umi/core/routes.ts @@ -78,7 +78,7 @@ export function getRoutes() { exact: true, meta: { filePath: 'docs/index.md', - updatedTime: 1607062509000, + updatedTime: 1607073241000, title: 'Dobux - React State Management Library', hero: { title: 'Dobux', @@ -229,7 +229,7 @@ export function getRoutes() { exact: true, meta: { filePath: 'docs/guide/examples.md', - updatedTime: 1607073242529, + updatedTime: 1607073241000, order: 3, slugs: [ { @@ -267,7 +267,7 @@ export function getRoutes() { exact: true, meta: { filePath: 'docs/guide/getting-started.md', - updatedTime: 1607062509000, + updatedTime: 1607073241000, order: 2, slugs: [ { @@ -315,7 +315,7 @@ export function getRoutes() { exact: true, meta: { filePath: 'docs/guide/index.md', - updatedTime: 1607071047788, + updatedTime: 1607073241000, order: 1, slugs: [ { diff --git a/src/core/Model.tsx b/src/core/Model.tsx index 9c70e10..f8147d9 100644 --- a/src/core/Model.tsx +++ b/src/core/Model.tsx @@ -7,6 +7,7 @@ import { ModelConfig, ModelConfigEffect, ModelContextProps, + Noop, StateSubscriber } from '../types' import { invariant } from '../utils/invariant' @@ -14,6 +15,7 @@ import { isObject } from '../utils/type' import { createProvider } from './createProvider' import { Container } from './Container' import { noop } from '../utils/func' +import { isDev } from '../common/env' interface ModelOptions { storeName: string @@ -23,25 +25,54 @@ interface ModelOptions { autoReset: boolean devTools: boolean } +interface ModelInstance { + [key: string]: number +} + +interface DevtoolExtension { + connect: (options: { name?: string }) => DevtoolInstance + disconnect: Noop +} + +interface DevtoolInstance { + subscribe: (cb: (message: { type: string; state: any }) => void) => Noop + send: (actionType: string, payload: Record) => void + init: (state: any) => void +} + +const devtoolExtension: DevtoolExtension = + isDev && typeof window !== 'undefined' && (window as any).__REDUX_DEVTOOLS_EXTENSION__ export class Model { - public Provider: React.FC + static instances: ModelInstance = Object.create(null) private model: ContextPropsModel private initialState: C['state'] - private container: Container + private container = new Container() private currentDispatcher: Dispatch = noop private isInternalUpdate = false + private instanceName: string + private devtoolInstance?: DevtoolInstance + private unsubscribeDevtool?: Noop + private isTimeTravel = false + + public Provider: React.FC private useContext: () => ModelContextProps constructor(private options: ModelOptions) { - const { name, config, rootModel } = options + const { storeName, name, config, rootModel } = options + + this.instanceName = `${storeName}/${name}` + + /* istanbul ignore else */ + if (!Model.instances[this.instanceName]) { + Model.instances[this.instanceName] = 0 + } this.initialState = config.state this.model = this.initModel(config) - this.container = new Container() rootModel[name] = this.model @@ -72,10 +103,30 @@ export class Model { } useEffect(() => { + if (this.options.devTools) { + // a Model only creates one devtool instance + if (Model.instances[this.instanceName] === 0) { + this.initDevTools() + } + + Model.instances[this.instanceName]++ + } + return (): void => { // unsubscribe when component unmount this.container.unsubscribe('state', subscriberRef.current as StateSubscriber) this.container.unsubscribe('effect', dispatcher) + + if (this.unsubscribeDevtool) { + Model.instances[this.instanceName]-- + + // disconnect after all dependent components are destroyed + /* istanbul ignore else */ + if (Model.instances[this.instanceName] <= 0) { + this.unsubscribeDevtool() + devtoolExtension?.disconnect() + } + } } }, []) @@ -108,6 +159,10 @@ export class Model { } private notify(name: string, state: C['state']): void { + if (this.devtoolInstance) { + this.devtoolInstance.send(`${this.options.name}/${name}`, state) + } + this.container.notify(state) } @@ -152,7 +207,12 @@ export class Model { this.model.state = newState - this.notify('setValues', newState) + /* istanbul ignore next */ + if (this.isTimeTravel) { + this.container.notify(newState) + } else { + this.notify('setValues', newState) + } } } @@ -244,4 +304,25 @@ export class Model { // @ts-ignore return config } + + private initDevTools(): void { + if (devtoolExtension) { + // https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md#name + this.devtoolInstance = devtoolExtension.connect({ + name: this.instanceName, + }) + + this.unsubscribeDevtool = this.devtoolInstance.subscribe( + /* istanbul ignore next */ message => { + if (message.type === 'DISPATCH' && message.state) { + this.isTimeTravel = true + this.model.reducers.setValues(JSON.parse(message.state)) + this.isTimeTravel = false + } + } + ) + + this.devtoolInstance.init(this.initialState) + } + } }