@fozy-labs/rx-toolkit 0.5.0-rc.1 → 0.5.0-rc.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/dist/common/react/index.d.ts +0 -3
- package/dist/common/react/index.js +0 -3
- package/dist/signals/base/ComputeCache.d.ts +17 -0
- package/dist/signals/base/ComputeCache.js +64 -0
- package/dist/signals/base/DependencyTracker.d.ts +1 -0
- package/dist/signals/base/ReadonlySignal.js +1 -0
- package/dist/signals/base/index.d.ts +1 -0
- package/dist/signals/base/index.js +1 -0
- package/dist/signals/react/useSignal.d.ts +1 -0
- package/dist/signals/react/useSignal.js +14 -2
- package/dist/signals/signals/Computed.d.ts +5 -2
- package/dist/signals/signals/Computed.js +11 -9
- package/dist/signals/signals/Signal.d.ts +0 -1
- package/dist/signals/signals/Signal.js +8 -7
- package/docs/migrations/0.5.0.md +16 -31
- package/docs/signals/README.md +24 -1
- package/docs/usage/react/README.md +0 -49
- package/package.json +3 -2
- package/dist/common/react/useObservable.d.ts +0 -12
- package/dist/common/react/useObservable.js +0 -48
- package/dist/common/react/useSyncObservable.d.ts +0 -16
- package/dist/common/react/useSyncObservable.js +0 -52
- package/dist/common/react/useUnmount.d.ts +0 -1
- package/dist/common/react/useUnmount.js +0 -24
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Кеш для хранения вычисленного значения и его зависимостей
|
|
3
|
+
*/
|
|
4
|
+
export declare class ComputeCache<T> {
|
|
5
|
+
private static _NO_VALUE;
|
|
6
|
+
private _cachedValue;
|
|
7
|
+
private _dependencies;
|
|
8
|
+
/**
|
|
9
|
+
* Проверяет, изменились ли зависимости с момента последнего вычисления
|
|
10
|
+
*/
|
|
11
|
+
isValid(): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Получает кешированное значение или вычисляет новое
|
|
14
|
+
*/
|
|
15
|
+
getOrCompute(computeFn: () => T): T;
|
|
16
|
+
clear(): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { DependencyTracker } from "../../signals";
|
|
2
|
+
/**
|
|
3
|
+
* Кеш для хранения вычисленного значения и его зависимостей
|
|
4
|
+
*/
|
|
5
|
+
export class ComputeCache {
|
|
6
|
+
static _NO_VALUE = Symbol('no-value');
|
|
7
|
+
_cachedValue = ComputeCache._NO_VALUE;
|
|
8
|
+
_dependencies = [];
|
|
9
|
+
/**
|
|
10
|
+
* Проверяет, изменились ли зависимости с момента последнего вычисления
|
|
11
|
+
*/
|
|
12
|
+
isValid() {
|
|
13
|
+
if (this._cachedValue === ComputeCache._NO_VALUE) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
// Проверяем, что все зависимости имеют те же значения
|
|
17
|
+
return this._dependencies.every(dep => {
|
|
18
|
+
try {
|
|
19
|
+
const currentValue = dep.peek();
|
|
20
|
+
return Object.is(currentValue, dep.lastValue);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Если не удалось получить значение, считаем кеш невалидным
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Получает кешированное значение или вычисляет новое
|
|
30
|
+
*/
|
|
31
|
+
getOrCompute(computeFn) {
|
|
32
|
+
if (this.isValid()) {
|
|
33
|
+
return this._cachedValue;
|
|
34
|
+
}
|
|
35
|
+
// Собираем зависимости во время вычисления
|
|
36
|
+
const dependencies = [];
|
|
37
|
+
const stopTracking = DependencyTracker.start((dep) => {
|
|
38
|
+
// Создаем peek-функцию для этой зависимости
|
|
39
|
+
dependencies.push({
|
|
40
|
+
peek: dep.peek,
|
|
41
|
+
lastValue: undefined, // Будет установлено после первого peek
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
try {
|
|
45
|
+
// Вычисляем значение
|
|
46
|
+
const result = computeFn();
|
|
47
|
+
// Получаем текущие значения зависимостей
|
|
48
|
+
dependencies.forEach(dep => {
|
|
49
|
+
dep.lastValue = dep.peek();
|
|
50
|
+
});
|
|
51
|
+
// Сохраняем результат и зависимости
|
|
52
|
+
this._cachedValue = result;
|
|
53
|
+
this._dependencies = dependencies;
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
stopTracking();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
clear() {
|
|
61
|
+
this._cachedValue = ComputeCache._NO_VALUE;
|
|
62
|
+
this._dependencies = [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useEventHandler } from "../../common/react";
|
|
2
3
|
export function useSignal(signal$) {
|
|
3
|
-
|
|
4
|
+
const subscribe = React.useCallback((update) => {
|
|
5
|
+
const subscription = signal$.obs.subscribe(() => {
|
|
6
|
+
update();
|
|
7
|
+
});
|
|
8
|
+
return () => {
|
|
9
|
+
subscription.unsubscribe();
|
|
10
|
+
};
|
|
11
|
+
}, [signal$]);
|
|
12
|
+
const getSnapshot = useEventHandler(() => {
|
|
13
|
+
return signal$.peek();
|
|
14
|
+
});
|
|
15
|
+
return React.useSyncExternalStore(subscribe, getSnapshot);
|
|
4
16
|
}
|
|
@@ -2,11 +2,14 @@ import { StateDevtoolsOptions } from "../../common/devtools";
|
|
|
2
2
|
import { ComputeFn } from "../../signals/types";
|
|
3
3
|
export declare class Computed<T> {
|
|
4
4
|
private _computeFn;
|
|
5
|
-
private options?;
|
|
6
5
|
private _ls;
|
|
7
6
|
readonly obs: import("rxjs").Observable<T>;
|
|
8
7
|
private _effect;
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Кеш для хранения вычисленного значения (без подписки) и его зависимостей
|
|
10
|
+
*/
|
|
11
|
+
private _computeCache;
|
|
12
|
+
constructor(_computeFn: () => T, options?: StateDevtoolsOptions);
|
|
10
13
|
get(): T;
|
|
11
14
|
peek(): T;
|
|
12
15
|
private _start;
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { distinctUntilChanged, ReplaySubject, share, map, finalize } from "rxjs";
|
|
2
|
-
import {
|
|
2
|
+
import { ComputeCache, DependencyTracker } from "../base";
|
|
3
3
|
import { Effect } from "./Effect";
|
|
4
|
+
import { Signal } from "./Signal";
|
|
4
5
|
export class Computed {
|
|
5
6
|
_computeFn;
|
|
6
|
-
options;
|
|
7
7
|
_ls;
|
|
8
8
|
obs;
|
|
9
9
|
_effect = null;
|
|
10
|
+
/**
|
|
11
|
+
* Кеш для хранения вычисленного значения (без подписки) и его зависимостей
|
|
12
|
+
*/
|
|
13
|
+
_computeCache = new ComputeCache();
|
|
10
14
|
constructor(_computeFn, options) {
|
|
11
15
|
this._computeFn = _computeFn;
|
|
12
|
-
this.options = options;
|
|
13
16
|
const lsOptions = {
|
|
14
17
|
base: Computed.name,
|
|
15
18
|
...(typeof options === 'string' ? { name: options } : options),
|
|
@@ -38,17 +41,15 @@ export class Computed {
|
|
|
38
41
|
return this._effect._getRang();
|
|
39
42
|
},
|
|
40
43
|
obs: this.obs,
|
|
44
|
+
peek: () => this.peek(),
|
|
41
45
|
});
|
|
42
|
-
|
|
43
|
-
if (v === Computed._EMPTY) {
|
|
44
|
-
return this._computeFn();
|
|
45
|
-
}
|
|
46
|
-
return v;
|
|
46
|
+
return this.peek();
|
|
47
47
|
}
|
|
48
48
|
peek() {
|
|
49
49
|
const v = this._ls.peek();
|
|
50
50
|
if (v === Computed._EMPTY) {
|
|
51
|
-
|
|
51
|
+
// Используем кеш для вычисления без создания подписки
|
|
52
|
+
return this._computeCache.getOrCompute(this._computeFn);
|
|
52
53
|
}
|
|
53
54
|
return v;
|
|
54
55
|
}
|
|
@@ -62,6 +63,7 @@ export class Computed {
|
|
|
62
63
|
}
|
|
63
64
|
this._ls.set(this._computeFn());
|
|
64
65
|
});
|
|
66
|
+
this._computeCache.clear();
|
|
65
67
|
if (initialValue === Computed._EMPTY) {
|
|
66
68
|
throw new Error('Computed value is not initialized');
|
|
67
69
|
}
|
|
@@ -11,7 +11,6 @@ export declare class Signal<T> {
|
|
|
11
11
|
peek(): T;
|
|
12
12
|
set(value: T): void;
|
|
13
13
|
get(): T;
|
|
14
|
-
protected _onChange(value: T): void;
|
|
15
14
|
private static _finalizationRegistry;
|
|
16
15
|
static create<T>(initialValue: T, options?: StateDevtoolsOptions): SignalFn<T>;
|
|
17
16
|
static compute<T>(computeFn: () => T, options?: StateDevtoolsOptions): import("../../signals/types").ComputeFn<T>;
|
|
@@ -21,21 +21,22 @@ export class Signal {
|
|
|
21
21
|
return this.bs$.getValue();
|
|
22
22
|
}
|
|
23
23
|
set(value) {
|
|
24
|
-
|
|
24
|
+
if (value === this.bs$.value) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
Batcher.run(() => {
|
|
28
|
+
this._stateDevtools?.(value);
|
|
29
|
+
this.bs$.next(value);
|
|
30
|
+
});
|
|
25
31
|
}
|
|
26
32
|
get() {
|
|
27
33
|
DependencyTracker.track({
|
|
28
34
|
getRang: () => this._rang,
|
|
29
35
|
obs: this.obs,
|
|
36
|
+
peek: () => this.peek(),
|
|
30
37
|
});
|
|
31
38
|
return this.bs$.getValue();
|
|
32
39
|
}
|
|
33
|
-
_onChange(value) {
|
|
34
|
-
Batcher.run(() => {
|
|
35
|
-
this._stateDevtools?.(value);
|
|
36
|
-
this.bs$.next(value);
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
40
|
// === static ===
|
|
40
41
|
static _finalizationRegistry = new FinalizationRegistry((heldValue) => {
|
|
41
42
|
heldValue('$COMPLETED');
|
package/docs/migrations/0.5.0.md
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
# Миграция с версии 0.4.x на 0.5.0
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
### Новый функциональный API для создания сигналов
|
|
4
5
|
|
|
5
6
|
```typescript
|
|
6
7
|
// Было
|
|
7
|
-
signal
|
|
8
|
-
signal.
|
|
8
|
+
const signal = new Signal(0); // ❌
|
|
9
|
+
const computed = new Computed(() => signal.value * 2); // ❌
|
|
10
|
+
signal.value += 1;
|
|
11
|
+
|
|
9
12
|
|
|
10
13
|
// Стало
|
|
11
|
-
signal.
|
|
12
|
-
|
|
14
|
+
const signal = Signal.create(0); // ✅
|
|
15
|
+
const computed = Signal.compute(() => signal() * 2); // ✅
|
|
16
|
+
signal.set(signal.peek() + 1);
|
|
13
17
|
```
|
|
14
18
|
|
|
19
|
+
|
|
15
20
|
### Сигналы больше не поддерживают `value`, `getValue()` и `next()`
|
|
16
21
|
|
|
17
22
|
```typescript
|
|
@@ -26,21 +31,16 @@ signal() // ✅
|
|
|
26
31
|
signal.get(); // ✅
|
|
27
32
|
signal.set(val); // ✅
|
|
28
33
|
```
|
|
34
|
+
### Сигналы больше не наследуются от Observable
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
```typescripttypescript
|
|
36
|
+
```typescript
|
|
33
37
|
// Было
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
comp.subscribe(() => {}); // ✅ берется из кеша
|
|
37
|
-
comp$.get(); // ✅ берется из кеша
|
|
38
|
+
signal.subscribe(fn); // ❌
|
|
39
|
+
signal.pipe() // ❌
|
|
38
40
|
|
|
39
41
|
// Стало
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
comp$.subscribe(() => {}); // ✅ пересчитывается
|
|
43
|
-
comp$.get(); // ✅ берется из кеша
|
|
42
|
+
signal.obs.subscribe(fn); // ✅
|
|
43
|
+
signal.obs.pipe() // ✅
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
### Нет необходимости вызывать `complete()` для Signal и Computed
|
|
@@ -56,18 +56,3 @@ const signal = Signal.create(0);
|
|
|
56
56
|
// Что-то делаем
|
|
57
57
|
// Ничего не нужно вызывать ✅
|
|
58
58
|
```
|
|
59
|
-
|
|
60
|
-
### Новый функциональный API для создания сигналов (рекомендуется)
|
|
61
|
-
|
|
62
|
-
```typescript
|
|
63
|
-
// Было
|
|
64
|
-
const signal = new Signal(0);
|
|
65
|
-
const computed = new Computed(() => signal.value * 2);
|
|
66
|
-
signal.value += 1; // ❌
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Стало
|
|
70
|
-
const signal = Signal.create(0);
|
|
71
|
-
const computed = Signal.compute(() => signal() * 2);
|
|
72
|
-
signal.set(signal.peek() + 1); // ✅
|
|
73
|
-
```
|
package/docs/signals/README.md
CHANGED
|
@@ -4,6 +4,12 @@ RxSignals — это реактивная система управления с
|
|
|
4
4
|
|
|
5
5
|
## Основные концепции
|
|
6
6
|
|
|
7
|
+
### Реактивность на основе значений
|
|
8
|
+
|
|
9
|
+
Сигналы (`Signal`) хранят текущее состояние, а производные сущности (`Computed`, `Effect`) автоматически отслеживают зависимости,
|
|
10
|
+
применяея кеширование на основе *значений*. Это приводит к тому, что в отличие от классического RxJS-подхода,
|
|
11
|
+
где каждое `next()` — это событие, в RxSignals важен именно факт *изменения значения*.
|
|
12
|
+
|
|
7
13
|
### Signal
|
|
8
14
|
|
|
9
15
|
Базовый класс для создания реактивных сигналов с изменяемым состоянием.
|
|
@@ -66,6 +72,24 @@ fullName.obs.subscribe(name => console.log(name));
|
|
|
66
72
|
- `peek()` — получить значение без регистрации зависимости
|
|
67
73
|
- `obs` — RxJS Observable для подписки на изменения
|
|
68
74
|
|
|
75
|
+
Также на данный момент Computed
|
|
76
|
+
|
|
77
|
+
+Важно: отличие от RxJS
|
|
78
|
+
+
|
|
79
|
+
+ - Computed: по умолчанию `Computed.obs` применяет `distinctUntilChanged()` — это значит, что подписчики не будут получать повторных эмиссий, если новое значение строго равно (`===`) предыдущему. Такое поведение предотвращает лишние рендеры и обработки, когда значение фактически не изменилось.
|
|
80
|
+
+ - Signal: базовый `Signal` использует `BehaviorSubject` и при вызове `set()` будет эмитить значение независимо от того, изменилось оно или нет (если вам нужно предотвратить повторные эмиссии для сигнала, применяйте операторы RxJS к `signal.obs`, например `signal.obs.pipe(distinctUntilChanged())`).
|
|
81
|
+
+
|
|
82
|
+
+Пример: если у вас есть `const total = Signal.compute(() => a.get() + b.get())`, то при изменении `a` или `b` `total.obs` сработает только если новое значение суммы отличается от предыдущего (по `===`). Если нужен особый критерий сравнения, используйте `distinctUntilChanged` с собственной функцией сравнения:
|
|
83
|
+
+
|
|
84
|
+
+```ts
|
|
85
|
+
+import { distinctUntilChanged } from 'rxjs';
|
|
86
|
+
+
|
|
87
|
+
+total.obs.pipe(distinctUntilChanged((prev, next) => /* ваша логика */));
|
|
88
|
+
+```
|
|
89
|
+
+
|
|
90
|
+
+Рекомендация: рассчитывайте на то, что `Computed` избавляет от лишних эмиссий по-умолчанию; если вам нужно другое поведение, применяйте RxJS-операторы к `obs` или создавайте `Computed`, возвращающий другую форму данных (например объект с версией) для более тонкого контроля.
|
|
91
|
+
+
|
|
92
|
+
|
|
69
93
|
### Effect
|
|
70
94
|
|
|
71
95
|
Создает побочный эффект, который автоматически выполняется при изменении используемых сигналов.
|
|
@@ -292,4 +316,3 @@ const user$ = Signal.create(null, {
|
|
|
292
316
|
## React интеграция
|
|
293
317
|
|
|
294
318
|
См. [React интеграция](../usage/react/README.md) для подробной информации о том, как использовать RxSignals в React приложениях.
|
|
295
|
-
|
|
@@ -34,55 +34,6 @@ function Counter() {
|
|
|
34
34
|
- Автоматическая подписка и отписка при размонтировании
|
|
35
35
|
- Не вызывает ре-рендер, если значение не изменилось
|
|
36
36
|
|
|
37
|
-
### useObservable
|
|
38
|
-
|
|
39
|
-
Подписывается на RxJS Observable и возвращает текущее значение.
|
|
40
|
-
|
|
41
|
-
```tsx
|
|
42
|
-
import { useObservable } from '@fozy-labs/rx-toolkit';
|
|
43
|
-
import { interval, map } from 'rxjs';
|
|
44
|
-
|
|
45
|
-
const timer$ = interval(1000).pipe(
|
|
46
|
-
map(count => `Timer: ${count}`)
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
function Timer() {
|
|
50
|
-
const timerValue = useObservable(timer$, 'Timer: 0');
|
|
51
|
-
|
|
52
|
-
return <div>{timerValue}</div>;
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
**Параметры:**
|
|
57
|
-
- `observable$` — RxJS Observable для подписки
|
|
58
|
-
- `initialValue` — начальное значение до первой эмиссии
|
|
59
|
-
|
|
60
|
-
**Особенности:**
|
|
61
|
-
- Требует начальное значение, если Observable асинхронный
|
|
62
|
-
- Автоматическая переподписка при смене Observable
|
|
63
|
-
|
|
64
|
-
### useSyncObservable
|
|
65
|
-
|
|
66
|
-
Синхронная версия `useObservable` для Observable, которые могут выдать значение немедленно.
|
|
67
|
-
|
|
68
|
-
```tsx
|
|
69
|
-
import { useSyncObservable } from '@fozy-labs/rx-toolkit';
|
|
70
|
-
import { BehaviorSubject } from 'rxjs';
|
|
71
|
-
|
|
72
|
-
const data$ = new BehaviorSubject('initial data');
|
|
73
|
-
|
|
74
|
-
function DataComponent() {
|
|
75
|
-
const data = useSyncObservable(data$);
|
|
76
|
-
|
|
77
|
-
return <div>Data: {data}</div>;
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
**Отличия от useObservable:**
|
|
82
|
-
- Не требует начального значения
|
|
83
|
-
- Подходит для BehaviorSubject, ReplaySubject и сигналов
|
|
84
|
-
- Выбросит ошибку, если Observable не эмитит значение синхронно
|
|
85
|
-
|
|
86
37
|
---
|
|
87
38
|
|
|
88
39
|
## RxQuery хуки
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fozy-labs/rx-toolkit",
|
|
3
|
-
"version": "0.5.0-rc.
|
|
3
|
+
"version": "0.5.0-rc.2",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"preferGlobal": false,
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"immer": "^10.1.3"
|
|
60
|
+
"immer": "^10.1.3",
|
|
61
|
+
"observable-hooks": "^4.2.4"
|
|
61
62
|
}
|
|
62
63
|
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { Observable } from 'rxjs';
|
|
2
|
-
/**
|
|
3
|
-
* Hook for automatically subscribing and unsubscribing from an Observable.
|
|
4
|
-
* If the Observable synchronously returns a value, it returns it.
|
|
5
|
-
* Else returns initialValue.
|
|
6
|
-
* If the value of the Observable changes, it resubscribes.
|
|
7
|
-
* If a value equal to the previous one arrives in the Observable,
|
|
8
|
-
* the previous value is returned without triggering a re-render.
|
|
9
|
-
* @param input$ Observable.
|
|
10
|
-
* @param initialValue Initial value that will be used before the Observable emits a value.
|
|
11
|
-
*/
|
|
12
|
-
export declare function useObservable<T>(input$: Observable<T>, initialValue: T): T;
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { BehaviorSubject } from 'rxjs';
|
|
3
|
-
import { useConstant } from "./useConstant";
|
|
4
|
-
import { useEventHandler } from "./useEventHandler";
|
|
5
|
-
const NONE = Symbol('NONE');
|
|
6
|
-
/**
|
|
7
|
-
* Hook for automatically subscribing and unsubscribing from an Observable.
|
|
8
|
-
* If the Observable synchronously returns a value, it returns it.
|
|
9
|
-
* Else returns initialValue.
|
|
10
|
-
* If the value of the Observable changes, it resubscribes.
|
|
11
|
-
* If a value equal to the previous one arrives in the Observable,
|
|
12
|
-
* the previous value is returned without triggering a re-render.
|
|
13
|
-
* @param input$ Observable.
|
|
14
|
-
* @param initialValue Initial value that will be used before the Observable emits a value.
|
|
15
|
-
*/
|
|
16
|
-
export function useObservable(input$, initialValue) {
|
|
17
|
-
const { subject$, subscription } = useConstant(() => {
|
|
18
|
-
const subject = new BehaviorSubject(NONE);
|
|
19
|
-
/**
|
|
20
|
-
* Check if the Observable synchronously returns a value,
|
|
21
|
-
* like BehaviorSubject or etc.
|
|
22
|
-
*/
|
|
23
|
-
const subscription = input$
|
|
24
|
-
.subscribe((value) => subject.next(value));
|
|
25
|
-
return {
|
|
26
|
-
subject$: subject,
|
|
27
|
-
subscription,
|
|
28
|
-
};
|
|
29
|
-
}, [input$]);
|
|
30
|
-
React.useEffect(() => {
|
|
31
|
-
return () => {
|
|
32
|
-
subscription.unsubscribe();
|
|
33
|
-
subject$.complete();
|
|
34
|
-
};
|
|
35
|
-
}, [subject$]);
|
|
36
|
-
const subscribe = React.useCallback((updateStore) => {
|
|
37
|
-
const subjectSubscription = subject$.subscribe(updateStore);
|
|
38
|
-
return () => {
|
|
39
|
-
subjectSubscription.unsubscribe();
|
|
40
|
-
};
|
|
41
|
-
}, [input$, subject$]);
|
|
42
|
-
const getSnapshot = useEventHandler(() => subject$.getValue());
|
|
43
|
-
let value = React.useSyncExternalStore(subscribe, getSnapshot);
|
|
44
|
-
if (value === NONE) {
|
|
45
|
-
value = initialValue;
|
|
46
|
-
}
|
|
47
|
-
return value;
|
|
48
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { Observable } from 'rxjs';
|
|
2
|
-
declare const NONE: unique symbol;
|
|
3
|
-
/**
|
|
4
|
-
* Hook for automatically subscribing and unsubscribing from an Observable.
|
|
5
|
-
* If the Observable synchronously returns a value, it returns it.
|
|
6
|
-
* Else if initialValue is provided, it returns it.
|
|
7
|
-
* Else (if initialValue is NONE and the Observable does not synchronously return a value),
|
|
8
|
-
* throws error.
|
|
9
|
-
* If the value of the Observable changes, it resubscribes.
|
|
10
|
-
* If a value equal to the previous one arrives in the Observable,
|
|
11
|
-
* the previous value is returned without triggering a re-render.
|
|
12
|
-
* @param input$ Observable.
|
|
13
|
-
* @param initialValue Initial value that will be used before the Observable emits a value.
|
|
14
|
-
*/
|
|
15
|
-
export declare function useSyncObservable<T>(input$: Observable<T>, initialValue?: T | typeof NONE): T;
|
|
16
|
-
export {};
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { BehaviorSubject } from 'rxjs';
|
|
3
|
-
import { useConstant } from "./useConstant";
|
|
4
|
-
import { useEventHandler } from "./useEventHandler";
|
|
5
|
-
import { useUnmount } from "../../common/react/useUnmount";
|
|
6
|
-
const NONE = Symbol('NONE');
|
|
7
|
-
/**
|
|
8
|
-
* Hook for automatically subscribing and unsubscribing from an Observable.
|
|
9
|
-
* If the Observable synchronously returns a value, it returns it.
|
|
10
|
-
* Else if initialValue is provided, it returns it.
|
|
11
|
-
* Else (if initialValue is NONE and the Observable does not synchronously return a value),
|
|
12
|
-
* throws error.
|
|
13
|
-
* If the value of the Observable changes, it resubscribes.
|
|
14
|
-
* If a value equal to the previous one arrives in the Observable,
|
|
15
|
-
* the previous value is returned without triggering a re-render.
|
|
16
|
-
* @param input$ Observable.
|
|
17
|
-
* @param initialValue Initial value that will be used before the Observable emits a value.
|
|
18
|
-
*/
|
|
19
|
-
export function useSyncObservable(input$, initialValue = NONE) {
|
|
20
|
-
const { subject$, subscription } = useConstant(() => {
|
|
21
|
-
const subject = new BehaviorSubject(NONE);
|
|
22
|
-
/**
|
|
23
|
-
* Check if the Observable synchronously returns a value,
|
|
24
|
-
* like BehaviorSubject or etc.
|
|
25
|
-
*/
|
|
26
|
-
const subscription = input$
|
|
27
|
-
.subscribe((value) => subject.next(value));
|
|
28
|
-
return {
|
|
29
|
-
subject$: subject,
|
|
30
|
-
subscription,
|
|
31
|
-
};
|
|
32
|
-
}, [input$]);
|
|
33
|
-
useUnmount(() => {
|
|
34
|
-
subscription.unsubscribe();
|
|
35
|
-
subject$.complete();
|
|
36
|
-
}, [subject$]);
|
|
37
|
-
const subscribe = React.useCallback((updateStore) => {
|
|
38
|
-
const subjectSubscription = subject$.subscribe(updateStore);
|
|
39
|
-
return () => {
|
|
40
|
-
subjectSubscription.unsubscribe();
|
|
41
|
-
};
|
|
42
|
-
}, [input$, subject$]);
|
|
43
|
-
const getSnapshot = useEventHandler(() => subject$.getValue());
|
|
44
|
-
let value = React.useSyncExternalStore(subscribe, getSnapshot);
|
|
45
|
-
if (value === NONE) {
|
|
46
|
-
value = initialValue;
|
|
47
|
-
}
|
|
48
|
-
if (value === NONE) {
|
|
49
|
-
throw new Error('Observable did not return a value and no initial value provided');
|
|
50
|
-
}
|
|
51
|
-
return value;
|
|
52
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function useUnmount(fn: () => void, deps?: any[]): void;
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
export function useUnmount(fn, deps = []) {
|
|
3
|
-
const preventUnmountRef = React.useRef(null);
|
|
4
|
-
React.useEffect(() => {
|
|
5
|
-
return () => {
|
|
6
|
-
let isPrevented = false;
|
|
7
|
-
preventUnmountRef.current = () => {
|
|
8
|
-
isPrevented = true;
|
|
9
|
-
};
|
|
10
|
-
new Promise((resolve) => {
|
|
11
|
-
resolve();
|
|
12
|
-
}).then(() => {
|
|
13
|
-
if (isPrevented)
|
|
14
|
-
return;
|
|
15
|
-
preventUnmountRef.current = null;
|
|
16
|
-
fn();
|
|
17
|
-
});
|
|
18
|
-
};
|
|
19
|
-
}, deps);
|
|
20
|
-
React.useEffect(() => {
|
|
21
|
-
preventUnmountRef.current?.();
|
|
22
|
-
preventUnmountRef.current = null;
|
|
23
|
-
}, deps);
|
|
24
|
-
}
|