bunja 0.0.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -2
- package/bunja.ts +30 -40
- package/package.json +2 -2
- package/tsconfig.json +5 -0
package/README.md
CHANGED
|
@@ -1,8 +1,59 @@
|
|
|
1
1
|
# Bunja
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Bunja is State Lifetime Manager for React. (Minified & gzipped size < 1kB)\
|
|
4
4
|
Heavily inspired by [Bunshi](https://github.com/saasquatch/bunshi).
|
|
5
5
|
|
|
6
6
|
> Definition: Bunja (分子 / 분자) - Korean for molecule, member or element.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Why is managing the lifetime of state necessary?
|
|
9
|
+
|
|
10
|
+
Global state managers like jotai or signals offer the advantage of declaratively describing state and effectively reducing render counts,
|
|
11
|
+
but they lack suitable methods for managing resources with a defined start and end.\
|
|
12
|
+
For example, consider establishing and closing a WebSocket connection or a modal form UI that appears temporarily and then disappears.
|
|
13
|
+
|
|
14
|
+
Bunja is a library designed to address these weaknesses.\
|
|
15
|
+
Each state defined with Bunja has a lifetime that begins when it is first depended on somewhere in the render tree and ends when all dependencies disappear.
|
|
16
|
+
|
|
17
|
+
Therefore, when writing a state to manage a WebSocket,
|
|
18
|
+
you only need to create a function that establishes the WebSocket connection and an dispose handler that terminates the connection.\
|
|
19
|
+
The library automatically tracks the actual usage period and calls the init and dispose as needed.
|
|
20
|
+
|
|
21
|
+
## So, do I no longer need jotai or other state management libraries?
|
|
22
|
+
|
|
23
|
+
No. Bunja focuses solely on managing the lifetime of state, so jotai and other state management libraries are still valuable.\
|
|
24
|
+
You can typically use jotai or something, and when lifetime management becomes necessary, you can wrap those states with bunja.
|
|
25
|
+
|
|
26
|
+
## How to use
|
|
27
|
+
|
|
28
|
+
Bunja provides two functions: `bunja` and `useBunja`.\
|
|
29
|
+
You can use `bunja` to define a state with a finite lifetime and use the `useBunja` hook to access that state.
|
|
30
|
+
|
|
31
|
+
### Defining a Bunja
|
|
32
|
+
|
|
33
|
+
You can define a bunja using the `bunja` function. When you access the defined bunja with the `useBunja` hook, a bunja instance is created.\
|
|
34
|
+
If all components in the render tree that refer to the bunja disappear, the bunja instance is automatically destroyed.
|
|
35
|
+
|
|
36
|
+
If you want to clean up resources when the bunja's lifetime ends, you can use the `Symbol.dispose` field.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const countBunja = bunja([], () => {
|
|
40
|
+
const countAtom = atom(0);
|
|
41
|
+
return {
|
|
42
|
+
countAtom,
|
|
43
|
+
[Symbol.dispose]() {
|
|
44
|
+
console.log("disposed");
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
function MyComponent() {
|
|
50
|
+
const { countAtom } = useBunja(countBunja);
|
|
51
|
+
const [count, setCount] = useAtom(countAtom);
|
|
52
|
+
// Your component logic here
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This code snippet defines a bunja that creates a `countAtom`.\
|
|
57
|
+
The `Symbol.dispose` method is used when the bunja instance is no longer referenced by any component in the render tree, allowing you to clean up resources appropriately.
|
|
58
|
+
|
|
59
|
+
TODO: context
|
package/bunja.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
|
|
3
|
+
disposePolyfill: {
|
|
4
|
+
const S = Symbol as any;
|
|
5
|
+
S.dispose ??= Symbol.for("dispose");
|
|
6
|
+
S.asyncDispose ??= Symbol.for("asyncDispose");
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
type Dep<T> = React.Context<T> | Bunja<T>;
|
|
4
10
|
|
|
5
11
|
class Bunja<T> {
|
|
@@ -7,32 +13,24 @@ class Bunja<T> {
|
|
|
7
13
|
public id: number,
|
|
8
14
|
public deps: Dep<any>[],
|
|
9
15
|
public contexts: React.Context<any>[],
|
|
10
|
-
public init: (args: any[]
|
|
16
|
+
public init: (...args: any[]) => T
|
|
11
17
|
) {}
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
unmount?: () => void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function bunja<T>(deps: [], init: () => BunjaInitResult<T>): Bunja<T>;
|
|
21
|
-
export function bunja<T, U>(
|
|
22
|
-
deps: [Dep<U>],
|
|
23
|
-
init: (u: U) => BunjaInitResult<T>
|
|
24
|
-
): Bunja<T>;
|
|
20
|
+
const bunjas: Record<string, BunjaInstance<any>> = {};
|
|
21
|
+
export function bunja<T>(deps: [], init: () => T): Bunja<T>;
|
|
22
|
+
export function bunja<T, U>(deps: [Dep<U>], init: (u: U) => T): Bunja<T>;
|
|
25
23
|
export function bunja<T, U, V>(
|
|
26
24
|
deps: [Dep<U>, Dep<V>],
|
|
27
|
-
init: (u: U, v: V) =>
|
|
25
|
+
init: (u: U, v: V) => T
|
|
28
26
|
): Bunja<T>;
|
|
29
27
|
export function bunja<T, U, V, W>(
|
|
30
28
|
deps: [Dep<U>, Dep<V>, Dep<W>],
|
|
31
|
-
init: (u: U, v: V, w: W) =>
|
|
29
|
+
init: (u: U, v: V, w: W) => T
|
|
32
30
|
): Bunja<T>;
|
|
33
31
|
export function bunja<T, const U extends any[]>(
|
|
34
32
|
deps: { [K in keyof U]: Dep<U[K]> },
|
|
35
|
-
init: (...args: U) =>
|
|
33
|
+
init: (...args: U) => T
|
|
36
34
|
): Bunja<T> {
|
|
37
35
|
const contexts = deps.filter(
|
|
38
36
|
(dep) => !(dep instanceof Bunja)
|
|
@@ -41,17 +39,12 @@ export function bunja<T, const U extends any[]>(
|
|
|
41
39
|
const dedupedContexts = Array.from(
|
|
42
40
|
new Set([...contexts, ...bunjas.flatMap((def) => def.contexts)])
|
|
43
41
|
);
|
|
44
|
-
return new Bunja(bunja.counter++, deps, dedupedContexts,
|
|
45
|
-
const noop = () => {};
|
|
46
|
-
const { value, mount = noop, unmount = noop } = init(...(args as U));
|
|
47
|
-
mount();
|
|
48
|
-
return new BunjaInstance(() => (dispose(), unmount()), value);
|
|
49
|
-
});
|
|
42
|
+
return new Bunja(bunja.counter++, deps, dedupedContexts, init);
|
|
50
43
|
}
|
|
51
44
|
bunja.counter = 0;
|
|
52
45
|
|
|
53
46
|
export function useBunja<T>(bunja: Bunja<T>): T {
|
|
54
|
-
const { id, deps, contexts } = bunja;
|
|
47
|
+
const { id, deps, contexts, init } = bunja;
|
|
55
48
|
const rid = useRid();
|
|
56
49
|
const tuples = contexts.map((c) => [c, React.useContext(c)] as const);
|
|
57
50
|
const scopes = tuples.map(([context, value]) => getScope(context, value));
|
|
@@ -64,28 +57,21 @@ export function useBunja<T>(bunja: Bunja<T>): T {
|
|
|
64
57
|
.map(({ id }) => id)
|
|
65
58
|
.sort()
|
|
66
59
|
.join(",")}`;
|
|
67
|
-
const
|
|
60
|
+
const instance = (bunjas[biid] ??= new BunjaInstance(biid, init(...args)));
|
|
68
61
|
React.useEffect(() => {
|
|
69
|
-
|
|
70
|
-
return () =>
|
|
71
|
-
}, [
|
|
62
|
+
instance.reg(rid);
|
|
63
|
+
return () => instance.dereg(rid);
|
|
64
|
+
}, [instance]);
|
|
72
65
|
React.useEffect(() => {
|
|
73
66
|
scopes.forEach((scope) => scope.reg(rid));
|
|
74
67
|
return () => scopes.forEach((scope) => scope.dereg(rid));
|
|
75
|
-
}, [rid]);
|
|
76
|
-
return
|
|
68
|
+
}, [rid, ...scopes]);
|
|
69
|
+
return instance.value;
|
|
77
70
|
}
|
|
78
71
|
|
|
79
72
|
const useRid = () => React.useState(() => useRid.counter++)[0];
|
|
80
73
|
useRid.counter = 0;
|
|
81
74
|
|
|
82
|
-
const bunjas: Record<string, BunjaInstance<any>> = {};
|
|
83
|
-
function getBunjaInstance<T>(bunja: Bunja<T>, biid: string, args: any[]) {
|
|
84
|
-
return (bunjas[biid] ??= bunja.init(args, () => {
|
|
85
|
-
delete bunjas[biid];
|
|
86
|
-
}));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
75
|
const scopes = new WeakMap<React.Context<any>, Map<any, Scope>>();
|
|
90
76
|
function getScope(context: React.Context<any>, value: any) {
|
|
91
77
|
const m = scopes.get(context) ?? scopes.set(context, new Map()).get(context)!;
|
|
@@ -98,7 +84,6 @@ getScope.counter = 0;
|
|
|
98
84
|
class RefCounter<T = number> {
|
|
99
85
|
#disposed = false;
|
|
100
86
|
refs = new Set<T>();
|
|
101
|
-
constructor(public dispose: () => void) {}
|
|
102
87
|
reg(reference: T) {
|
|
103
88
|
this.refs.add(reference);
|
|
104
89
|
}
|
|
@@ -108,15 +93,19 @@ class RefCounter<T = number> {
|
|
|
108
93
|
if (this.#disposed) return;
|
|
109
94
|
if (this.refs.size < 1) {
|
|
110
95
|
this.#disposed = true;
|
|
111
|
-
this.dispose();
|
|
96
|
+
this[Symbol.dispose]();
|
|
112
97
|
}
|
|
113
98
|
});
|
|
114
99
|
}
|
|
115
100
|
}
|
|
116
101
|
|
|
117
102
|
class BunjaInstance<T> extends RefCounter {
|
|
118
|
-
constructor(
|
|
119
|
-
super(
|
|
103
|
+
constructor(public biid: string, public value: T) {
|
|
104
|
+
super();
|
|
105
|
+
}
|
|
106
|
+
[Symbol.dispose]() {
|
|
107
|
+
delete bunjas[this.biid];
|
|
108
|
+
(this.value[Symbol.asyncDispose] ?? this.value[Symbol.dispose])?.();
|
|
120
109
|
}
|
|
121
110
|
}
|
|
122
111
|
|
|
@@ -127,6 +116,7 @@ class Scope extends RefCounter {
|
|
|
127
116
|
public value: any,
|
|
128
117
|
public id: number
|
|
129
118
|
) {
|
|
130
|
-
super(
|
|
119
|
+
super();
|
|
120
|
+
this[Symbol.dispose] = dispose;
|
|
131
121
|
}
|
|
132
122
|
}
|
package/package.json
CHANGED