Skip to content

Commit

Permalink
fix: hook params update in microtask queue
Browse files Browse the repository at this point in the history
  • Loading branch information
betula committed Dec 24, 2023
1 parent f3e2036 commit ed6cae5
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 9 deletions.
17 changes: 14 additions & 3 deletions src/hook.ts
Original file line number Diff line number Diff line change
@@ -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<T>(Class: (() => T) | (new () => T)): (() => T);
export function hook<T, M>(Class: ((params: SignalReadonly<M>) => T) | (new (params: SignalReadonly<M>) => T)): ((params: M) => T);
Expand All @@ -11,18 +12,28 @@ export function hook<T, M>(Class: ((params?: SignalReadonly<M>) => 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<M>) => T)(wrapped)
: new (Class as new (params?: SignalReadonly<M>) => 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;
}
}
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export {

export { observer } from 'mobx-react-lite';
export {
untracked,
transaction,
makeAutoObservable,
makeObservable,
Expand All @@ -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';
export { hook } from './hook';
export { untracked } from './untracked';
5 changes: 3 additions & 2 deletions src/service.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { untracked } from 'mobx';
import { provide, destroy } from 'provi/client'

const INSTANTIATE_KEY = Symbol('instantiate');
Expand All @@ -22,15 +23,15 @@ export const service: ServiceFactory = (<T>(Class: (() => T) | (new () => T)) =>
return;
}
if (!instance) {
instance = provide(Class)
instance = untracked(() => provide(Class));
};
if (prop !== INSTANTIATE_KEY) {
return instance[prop];
}
},
set(_target, prop, value) {
if (!instance) {
instance = provide(Class);
instance = untracked(() => provide(Class));
}
instance[prop] = value;
return true;
Expand Down
16 changes: 16 additions & 0 deletions src/untracked.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { untracked as untrackedOrigin } from 'mobx';

interface Untracked {
<T>(action: () => T): T;
func: <T extends any[], R>(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));
}
}
12 changes: 10 additions & 2 deletions test/hook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand Down
27 changes: 27 additions & 0 deletions test/untracked.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});

0 comments on commit ed6cae5

Please sign in to comment.