diff --git a/src/hook.ts b/src/hook.ts index 6d29cf5..6b6e749 100644 --- a/src/hook.ts +++ b/src/hook.ts @@ -1,6 +1,7 @@ import React from 'react'; import { collect, unsubscriber, run } from 'unsubscriber'; import { signal, wrap, SignalReadonly } from './signal'; +import { untracked } from './untracked'; export function hook(Class: (() => T) | (new () => T)): (() => T); export function hook(Class: ((params: SignalReadonly) => T) | (new (params: SignalReadonly) => T)): ((params: M) => T); @@ -11,18 +12,28 @@ export function hook(Class: ((params?: SignalReadonly) => T) | (new (pa const wrapped = wrap(signalParams.get); const unsubs = unsubscriber(); return [ - collect(unsubs, () => ( + collect(unsubs, untracked.func(() => ( Class.prototype === void 0 ? (Class as (params?: SignalReadonly) => T)(wrapped) : new (Class as new (params?: SignalReadonly) => T)(wrapped) - )), + ))), unsubs, signalParams ] }, []); React.useEffect(() => () => run(unsubs), [unsubs]); - signalParams(params); + + + // For prevent the error + // Error: Cannot update a component (`Unknown`) while rendering a different component (`Unknown`). + // We should add time for render phase finished before update params, + // that can initiate next update sycle inside current. We use Promise microtask queue for prevent it. + // (I believe one time MobX will implement "Unit of Work" pattern for observer updating) + + Promise.resolve().then(() => { + signalParams(params); + }); return instance; } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 5a562b7..2484a70 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,6 @@ export { export { observer } from 'mobx-react-lite'; export { - untracked, transaction, makeAutoObservable, makeObservable, @@ -24,4 +23,5 @@ export { autorun, reaction, sync, when } from './reaction'; export { event, type Event, type LightEvent } from './event'; export { service } from './service'; -export { hook } from './hook'; \ No newline at end of file +export { hook } from './hook'; +export { untracked } from './untracked'; \ No newline at end of file diff --git a/src/service.ts b/src/service.ts index ec5963b..63b73ea 100644 --- a/src/service.ts +++ b/src/service.ts @@ -1,3 +1,4 @@ +import { untracked } from 'mobx'; import { provide, destroy } from 'provi/client' const INSTANTIATE_KEY = Symbol('instantiate'); @@ -22,7 +23,7 @@ export const service: ServiceFactory = ((Class: (() => T) | (new () => T)) => return; } if (!instance) { - instance = provide(Class) + instance = untracked(() => provide(Class)); }; if (prop !== INSTANTIATE_KEY) { return instance[prop]; @@ -30,7 +31,7 @@ export const service: ServiceFactory = ((Class: (() => T) | (new () => T)) => }, set(_target, prop, value) { if (!instance) { - instance = provide(Class); + instance = untracked(() => provide(Class)); } instance[prop] = value; return true; diff --git a/src/untracked.ts b/src/untracked.ts new file mode 100644 index 0000000..0c762a4 --- /dev/null +++ b/src/untracked.ts @@ -0,0 +1,16 @@ +import { untracked as untrackedOrigin } from 'mobx'; + +interface Untracked { + (action: () => T): T; + func: (fn: (...args: T) => R) => ((...args: T) => R); +} + +export const untracked: Untracked = (action) => { + return untrackedOrigin(action); +} + +untracked.func = (fn) => { + return function handle() { + return untracked(() => fn.apply(this, arguments)); + } +} \ No newline at end of file diff --git a/test/hook.test.ts b/test/hook.test.ts index 23735cf..f5ff187 100644 --- a/test/hook.test.ts +++ b/test/hook.test.ts @@ -2,6 +2,8 @@ import React from 'react'; import { scope } from 'unsubscriber'; import { SignalReadonly, autorun, hook, signal, un } from "../src"; +const waitNextTick = () => new Promise(resolve => setTimeout(resolve, 0)); + let useMemoCache; let unmount; let unsubs; @@ -63,7 +65,7 @@ it('hook works', () => { expect(destroy_spy).toBeCalled(); }); -it('hook array params works', () => { +it('hook array params works', async () => { const create_spy = jest.fn(); const destroy_spy = jest.fn(); const params_spy = jest.fn(); @@ -85,12 +87,15 @@ it('hook array params works', () => { expect(params_spy).toBeCalledWith([10, 'a']); params_spy.mockClear(); expect(inst).toBe(useA([10, 'a'])); + await waitNextTick(); expect(params_spy).not.toBeCalled(); expect(inst).toBe(useA([10, 'b'])); + await waitNextTick(); expect(params_spy).toBeCalledWith([10, 'b']); params_spy.mockClear(); expect(inst).toBe(useA([10, 'b'])); + await waitNextTick(); expect(params_spy).not.toBeCalled(); expect(create_spy).toBeCalled(); @@ -100,7 +105,7 @@ it('hook array params works', () => { expect(destroy_spy).toBeCalled(); }); -it('hook struct params works', () => { +it('hook struct params works', async () => { const create_spy = jest.fn(); const destroy_spy = jest.fn(); const params_spy = jest.fn(); @@ -122,12 +127,15 @@ it('hook struct params works', () => { expect(params_spy).toBeCalledWith({a: 10, b: 'a'}); params_spy.mockClear(); expect(inst).toBe(useA({a: 10, b: 'a'})); + await waitNextTick(); expect(params_spy).not.toBeCalled(); expect(inst).toBe(useA({a: 10, b: 'b'})); + await waitNextTick(); expect(params_spy).toBeCalledWith({a: 10, b: 'b'}); params_spy.mockClear(); expect(inst).toBe(useA({a: 10, b: 'b'})); + await waitNextTick(); expect(params_spy).not.toBeCalled(); expect(create_spy).toBeCalled(); diff --git a/test/untracked.test.ts b/test/untracked.test.ts new file mode 100644 index 0000000..5a5db43 --- /dev/null +++ b/test/untracked.test.ts @@ -0,0 +1,27 @@ +import { untracked, signal, autorun } from "../src"; + +it('untracked should work', () => { + const spy = jest.fn(); + + const s = signal(0); + autorun(() => { + spy(untracked(s.get)); + }) + + expect(spy).toBeCalledWith(0); spy.mockClear(); + s(1); + expect(spy).not.toBeCalled(); +}); + +it('untracked func should work', () => { + const spy = jest.fn(); + + const s = signal(0); + autorun(untracked.func(() => { + spy(s.value); + })) + + expect(spy).toBeCalledWith(0); spy.mockClear(); + s(1); + expect(spy).not.toBeCalled(); +}); \ No newline at end of file