-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathuseObservable.ts
More file actions
111 lines (92 loc) · 2.91 KB
/
useObservable.ts
File metadata and controls
111 lines (92 loc) · 2.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import { reaction } from "mobx";
import {
IAnyStateTreeNode,
IStateTreeNode,
getMembers,
isStateTreeNode,
} from "mobx-state-tree";
import { useCallback, useLayoutEffect, useRef, useState } from "react";
const emptyObject = {};
type ViewArgs<T> = T extends (...args: infer A) => unknown ? A : never;
class Path<T extends IAnyStateTreeNode> {
parts: (string | symbol)[];
args?: ViewArgs<T>;
constructor(parts: (string | symbol)[], args?: ViewArgs<T>) {
this.parts = parts;
this.args = args;
}
toString() {
return this.parts.join(".");
}
}
export function useObservable<T extends IStateTreeNode>(
model: T,
debug?: boolean
): T {
const [, setState] = useState(1);
const forceRender = useCallback(() => setState((n) => -n), []);
const pathsAccessedInRender = useRef<Set<Path<T>>>(new Set());
pathsAccessedInRender.current = new Set();
useLayoutEffect(() => {
const disposer = reaction(
function expression() {
if (debug) {
console.log("Paths accessed in render:");
pathsAccessedInRender.current.forEach((path) => {
console.log(" ", path.toString());
if (path.args) {
console.log(" This is a view fn, with args:", path.args);
}
});
}
for (const path of pathsAccessedInRender.current) {
let current: IAnyStateTreeNode = model;
for (const part of path.parts) {
current = current[part as keyof typeof current];
if (typeof current === "function" && path.args) {
current = current(...path.args);
}
}
}
return []; // If anything changes we want to re-render
},
function effect() {
forceRender();
},
{
fireImmediately: false,
}
);
return () => {
disposer();
};
}, [forceRender, model, debug]);
function makeProxy(instance: IAnyStateTreeNode, path: (string | symbol)[]) {
return new Proxy(emptyObject, {
get(_, key) {
const value = instance[key as keyof typeof instance];
const newPath = new Path([...path, key as string]);
pathsAccessedInRender.current.add(newPath);
if (isStateTreeNode(value)) {
return makeProxy(value, [...path, key]);
}
if (typeof value === "function") {
const members = getMembers(instance);
if (members.views.includes(key as string)) {
const view = value.bind(instance);
const argumentWatcher = (...args: ViewArgs<typeof view>) => {
const newPath = new Path([...path, key as string], args);
pathsAccessedInRender.current.add(newPath);
return view(...args);
};
return argumentWatcher;
} else {
return value.bind(instance);
}
}
return value;
},
});
}
return makeProxy(model, []) as T;
}