react-native-hox 0.0.1-beta
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.en.md +36 -0
- package/README.md +139 -0
- package/dist/index.d.mts +65 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +277 -0
- package/dist/index.mjs +273 -0
- package/package.json +53 -0
package/README.en.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# react-native-hox
|
|
2
|
+
|
|
3
|
+
#### Description
|
|
4
|
+
{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**}
|
|
5
|
+
|
|
6
|
+
#### Software Architecture
|
|
7
|
+
Software architecture description
|
|
8
|
+
|
|
9
|
+
#### Installation
|
|
10
|
+
|
|
11
|
+
1. xxxx
|
|
12
|
+
2. xxxx
|
|
13
|
+
3. xxxx
|
|
14
|
+
|
|
15
|
+
#### Instructions
|
|
16
|
+
|
|
17
|
+
1. xxxx
|
|
18
|
+
2. xxxx
|
|
19
|
+
3. xxxx
|
|
20
|
+
|
|
21
|
+
#### Contribution
|
|
22
|
+
|
|
23
|
+
1. Fork the repository
|
|
24
|
+
2. Create Feat_xxx branch
|
|
25
|
+
3. Commit your code
|
|
26
|
+
4. Create Pull Request
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
#### Gitee Feature
|
|
30
|
+
|
|
31
|
+
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
|
|
32
|
+
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
|
|
33
|
+
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
|
|
34
|
+
4. The most valuable open source project [GVP](https://gitee.com/gvp)
|
|
35
|
+
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
|
|
36
|
+
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# react-native-hox
|
|
2
|
+
|
|
3
|
+
**为复杂业务场景而生的状态管理解决方案**。
|
|
4
|
+
|
|
5
|
+
我们把 API 收敛到一个:`createModel`。组件内像 Hook 一样用,组件外(如网络请求拦截器)也能直接读取/订阅最新状态。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 核心特性
|
|
10
|
+
|
|
11
|
+
* **一个 API**:`createModel` 创建全局模型。
|
|
12
|
+
* **组件内/外都能用**:组件内 `model()` / `model(selector)` 订阅更新;组件外用 `model.data / model.getState()` 获取最新快照(不触发渲染)。
|
|
13
|
+
* **内置优化**:对象更新自动合并、无变化不更新(减少无意义重渲染)。
|
|
14
|
+
* **生产力优先**:可选持久化(Persist)、可选 Web 多窗口同步(BroadcastChannel)。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 快速上手(推荐)
|
|
19
|
+
|
|
20
|
+
### 安装
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install react-native-hox
|
|
24
|
+
# or
|
|
25
|
+
yarn add react-native-hox
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 创建一个全局模型
|
|
29
|
+
|
|
30
|
+
适用于用户信息、全局配置、表单草稿、token 等场景。
|
|
31
|
+
**完全不需要 Provider,也不需要修改入口文件。**
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// store.ts
|
|
35
|
+
import { createModel } from 'react-native-hox';
|
|
36
|
+
|
|
37
|
+
// 定义一个支持持久化的用户状态
|
|
38
|
+
// createModel(initialState, options?)
|
|
39
|
+
export const userModel = createModel({
|
|
40
|
+
name: 'Guest',
|
|
41
|
+
token: '',
|
|
42
|
+
theme: 'light'
|
|
43
|
+
}, {
|
|
44
|
+
persist: 'user_store', // 开启持久化,自动写入 Storage
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// 推荐:在组件内使用 useXxxModel 作为 Hook 别名(同一个对象)
|
|
48
|
+
// 这样更符合 Hook 心智模型,也能被 eslint-plugin-react-hooks 正确识别
|
|
49
|
+
export const useUserModel = userModel;
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**组件中使用:**
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { useUserModel } from './store';
|
|
56
|
+
|
|
57
|
+
function UserProfile() {
|
|
58
|
+
// 直接调用 model() 获取响应式 state
|
|
59
|
+
const user = useUserModel.useState();
|
|
60
|
+
|
|
61
|
+
const login = () => {
|
|
62
|
+
// 自动合并更新:只更新 token 和 name,theme 保持不变
|
|
63
|
+
useUserModel.setState({
|
|
64
|
+
token: 'xyz',
|
|
65
|
+
name: 'User'
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<View>
|
|
71
|
+
<Text>{user.name}</Text>
|
|
72
|
+
<Button onPress={login}>Login</Button>
|
|
73
|
+
</View>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
> 说明:组件渲染是否更新,取决于你是否用 `useUserModel.useState()` / `useUserModel.useState(selector)`(或直接 `useUserModel()`)订阅了状态;`userModel.data` / `useUserModel.data` 是快照不会触发渲染。`setState` 如果没有真正改变值会被自动忽略(减少无意义重渲染)。
|
|
79
|
+
|
|
80
|
+
**什么时候用 `useXxxModel()` / `getState()` / `data`?**
|
|
81
|
+
|
|
82
|
+
- 组件渲染里:用 `useUserModel.useState()`(或 `useUserModel.useState(s => s.token)`),它会订阅更新,状态变了组件会跟着重渲染
|
|
83
|
+
- 组件外(网络请求拦截器、工具函数、非 React 环境):用 `userModel.getState()` / `userModel.data` 读取最新快照,或 `userModel.subscribe(...)` 订阅
|
|
84
|
+
- 不建议在组件渲染里用 `useUserModel.getState()` / `useUserModel.data`:它们是“快照读取”,不会触发组件更新,容易出现 UI 不刷新
|
|
85
|
+
|
|
86
|
+
**组件外使用:**
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { userModel } from './store';
|
|
90
|
+
|
|
91
|
+
// 在 Axios 拦截器中读取 Token
|
|
92
|
+
axios.interceptors.request.use(config => {
|
|
93
|
+
const { token } = userModel.data; // 直接获取最新快照(非 Hook)
|
|
94
|
+
if (token) config.headers.Authorization = token;
|
|
95
|
+
return config;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// 在任意函数中更新状态
|
|
99
|
+
export function logout() {
|
|
100
|
+
userModel.setState({ token: '', name: 'Guest' });
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 全局配置(可选)
|
|
107
|
+
|
|
108
|
+
`persist` 默认会使用 **Web 端的 `localStorage`(零配置)**。在 React Native 环境没有默认持久化存储,需要你通过 `setup` 注入(也可以用 `setup` 覆盖 Web 默认实现)。
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { setup } from 'react-native-hox';
|
|
112
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
113
|
+
import { MMKV } from 'react-native-mmkv';
|
|
114
|
+
|
|
115
|
+
// 适配 AsyncStorage
|
|
116
|
+
setup({ storage: AsyncStorage });
|
|
117
|
+
|
|
118
|
+
// 或者适配 MMKV
|
|
119
|
+
const storage = new MMKV();
|
|
120
|
+
setup({
|
|
121
|
+
storage: {
|
|
122
|
+
getItem: (key) => storage.getString(key),
|
|
123
|
+
setItem: (key, val) => storage.set(key, val),
|
|
124
|
+
removeItem: (key) => storage.delete(key),
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 高级特性
|
|
130
|
+
|
|
131
|
+
### Web 端多窗口同步
|
|
132
|
+
|
|
133
|
+
如果你在使用 `react-native-web`,或者在 WebView 场景下需要多 Tab 状态同步,可以开启 `sync` 选项。
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
const userModel = createModel({ ... }, {
|
|
137
|
+
persist: 'user',
|
|
138
|
+
});
|
|
139
|
+
```
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
type Listener<T> = (state: T, prevState: T) => void;
|
|
2
|
+
type EqualityFn<T> = (a: T, b: T) => boolean;
|
|
3
|
+
|
|
4
|
+
type Selector<T, U> = (state: T) => U;
|
|
5
|
+
type ModelHook<T> = {
|
|
6
|
+
(): T;
|
|
7
|
+
<U>(selector: Selector<T, U>, equalityFn?: EqualityFn<U>): U;
|
|
8
|
+
};
|
|
9
|
+
interface PersistOptions<T> {
|
|
10
|
+
key: string;
|
|
11
|
+
debounce?: number;
|
|
12
|
+
beforePersist?: (state: T) => any;
|
|
13
|
+
beforeRecover?: (value: any) => T;
|
|
14
|
+
}
|
|
15
|
+
interface CreateModelOptions<T> {
|
|
16
|
+
/**
|
|
17
|
+
* 开启持久化。建议直接传 key:persist: 'user_store'
|
|
18
|
+
*/
|
|
19
|
+
persist?: string | PersistOptions<T>;
|
|
20
|
+
/**
|
|
21
|
+
* Web 多窗口同步(BroadcastChannel)。需要 persist key 才能工作。
|
|
22
|
+
*/
|
|
23
|
+
sync?: boolean;
|
|
24
|
+
}
|
|
25
|
+
interface SubscribeWithSelectorOptions<U> {
|
|
26
|
+
equalityFn?: EqualityFn<U>;
|
|
27
|
+
fireImmediately?: boolean;
|
|
28
|
+
}
|
|
29
|
+
interface Model<T> {
|
|
30
|
+
(): T;
|
|
31
|
+
<U>(selector: Selector<T, U>, equalityFn?: EqualityFn<U>): U;
|
|
32
|
+
/**
|
|
33
|
+
* 更显式的 Hook 入口(等价于直接调用 model())
|
|
34
|
+
* 推荐在组件内写:const state = useUserModel.useState()
|
|
35
|
+
*/
|
|
36
|
+
useState: ModelHook<T>;
|
|
37
|
+
/**
|
|
38
|
+
* useState 的别名(更短),保留给喜欢 `useXxx.use()` 风格的团队
|
|
39
|
+
*/
|
|
40
|
+
use: ModelHook<T>;
|
|
41
|
+
getState: () => T;
|
|
42
|
+
setState: (partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: boolean) => void;
|
|
43
|
+
subscribe: {
|
|
44
|
+
(listener: Listener<T>): () => void;
|
|
45
|
+
<U>(selector: Selector<T, U>, listener: (selected: U, prevSelected: U) => void, options?: SubscribeWithSelectorOptions<U>): () => void;
|
|
46
|
+
};
|
|
47
|
+
destroy: () => void;
|
|
48
|
+
reset: () => void;
|
|
49
|
+
data: T;
|
|
50
|
+
}
|
|
51
|
+
declare function createModel<T>(initialState: T | (() => T), options?: CreateModelOptions<T>): Model<T>;
|
|
52
|
+
|
|
53
|
+
interface StorageEngine {
|
|
54
|
+
getItem: (key: string) => string | null | Promise<string | null>;
|
|
55
|
+
setItem: (key: string, value: string) => void | Promise<void>;
|
|
56
|
+
removeItem: (key: string) => void | Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
interface Config {
|
|
59
|
+
storage?: StorageEngine;
|
|
60
|
+
}
|
|
61
|
+
declare const setup: (options: Config) => void;
|
|
62
|
+
|
|
63
|
+
declare const shallow: EqualityFn<any>;
|
|
64
|
+
|
|
65
|
+
export { type CreateModelOptions, type Model, type PersistOptions, type StorageEngine, type SubscribeWithSelectorOptions, createModel, setup, shallow };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
type Listener<T> = (state: T, prevState: T) => void;
|
|
2
|
+
type EqualityFn<T> = (a: T, b: T) => boolean;
|
|
3
|
+
|
|
4
|
+
type Selector<T, U> = (state: T) => U;
|
|
5
|
+
type ModelHook<T> = {
|
|
6
|
+
(): T;
|
|
7
|
+
<U>(selector: Selector<T, U>, equalityFn?: EqualityFn<U>): U;
|
|
8
|
+
};
|
|
9
|
+
interface PersistOptions<T> {
|
|
10
|
+
key: string;
|
|
11
|
+
debounce?: number;
|
|
12
|
+
beforePersist?: (state: T) => any;
|
|
13
|
+
beforeRecover?: (value: any) => T;
|
|
14
|
+
}
|
|
15
|
+
interface CreateModelOptions<T> {
|
|
16
|
+
/**
|
|
17
|
+
* 开启持久化。建议直接传 key:persist: 'user_store'
|
|
18
|
+
*/
|
|
19
|
+
persist?: string | PersistOptions<T>;
|
|
20
|
+
/**
|
|
21
|
+
* Web 多窗口同步(BroadcastChannel)。需要 persist key 才能工作。
|
|
22
|
+
*/
|
|
23
|
+
sync?: boolean;
|
|
24
|
+
}
|
|
25
|
+
interface SubscribeWithSelectorOptions<U> {
|
|
26
|
+
equalityFn?: EqualityFn<U>;
|
|
27
|
+
fireImmediately?: boolean;
|
|
28
|
+
}
|
|
29
|
+
interface Model<T> {
|
|
30
|
+
(): T;
|
|
31
|
+
<U>(selector: Selector<T, U>, equalityFn?: EqualityFn<U>): U;
|
|
32
|
+
/**
|
|
33
|
+
* 更显式的 Hook 入口(等价于直接调用 model())
|
|
34
|
+
* 推荐在组件内写:const state = useUserModel.useState()
|
|
35
|
+
*/
|
|
36
|
+
useState: ModelHook<T>;
|
|
37
|
+
/**
|
|
38
|
+
* useState 的别名(更短),保留给喜欢 `useXxx.use()` 风格的团队
|
|
39
|
+
*/
|
|
40
|
+
use: ModelHook<T>;
|
|
41
|
+
getState: () => T;
|
|
42
|
+
setState: (partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: boolean) => void;
|
|
43
|
+
subscribe: {
|
|
44
|
+
(listener: Listener<T>): () => void;
|
|
45
|
+
<U>(selector: Selector<T, U>, listener: (selected: U, prevSelected: U) => void, options?: SubscribeWithSelectorOptions<U>): () => void;
|
|
46
|
+
};
|
|
47
|
+
destroy: () => void;
|
|
48
|
+
reset: () => void;
|
|
49
|
+
data: T;
|
|
50
|
+
}
|
|
51
|
+
declare function createModel<T>(initialState: T | (() => T), options?: CreateModelOptions<T>): Model<T>;
|
|
52
|
+
|
|
53
|
+
interface StorageEngine {
|
|
54
|
+
getItem: (key: string) => string | null | Promise<string | null>;
|
|
55
|
+
setItem: (key: string, value: string) => void | Promise<void>;
|
|
56
|
+
removeItem: (key: string) => void | Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
interface Config {
|
|
59
|
+
storage?: StorageEngine;
|
|
60
|
+
}
|
|
61
|
+
declare const setup: (options: Config) => void;
|
|
62
|
+
|
|
63
|
+
declare const shallow: EqualityFn<any>;
|
|
64
|
+
|
|
65
|
+
export { type CreateModelOptions, type Model, type PersistOptions, type StorageEngine, type SubscribeWithSelectorOptions, createModel, setup, shallow };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
|
|
5
|
+
// src/createModel.ts
|
|
6
|
+
|
|
7
|
+
// src/config.ts
|
|
8
|
+
function getDefaultWebStorage() {
|
|
9
|
+
try {
|
|
10
|
+
const anyGlobal = globalThis;
|
|
11
|
+
const storage = anyGlobal == null ? void 0 : anyGlobal.localStorage;
|
|
12
|
+
if (!storage) {
|
|
13
|
+
return void 0;
|
|
14
|
+
}
|
|
15
|
+
const testKey = "__react_native_hox_test__";
|
|
16
|
+
storage.setItem(testKey, "1");
|
|
17
|
+
storage.removeItem(testKey);
|
|
18
|
+
return {
|
|
19
|
+
getItem: (key) => storage.getItem(key),
|
|
20
|
+
setItem: (key, value) => storage.setItem(key, value),
|
|
21
|
+
removeItem: (key) => storage.removeItem(key)
|
|
22
|
+
};
|
|
23
|
+
} catch {
|
|
24
|
+
return void 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
var config = {
|
|
28
|
+
storage: getDefaultWebStorage()
|
|
29
|
+
};
|
|
30
|
+
var setup = (options) => {
|
|
31
|
+
Object.assign(config, options);
|
|
32
|
+
};
|
|
33
|
+
var getConfig = () => config;
|
|
34
|
+
|
|
35
|
+
// src/vanilla.ts
|
|
36
|
+
var createStore = (initialState) => {
|
|
37
|
+
let state = initialState;
|
|
38
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
39
|
+
const getState = () => state;
|
|
40
|
+
const setState = (partial, replace = false) => {
|
|
41
|
+
const nextState = typeof partial === "function" ? partial(state) : partial;
|
|
42
|
+
if (Object.is(nextState, state)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const previousState = state;
|
|
46
|
+
const isNextObject = typeof nextState === "object" && nextState !== null;
|
|
47
|
+
const isPrevObject = typeof previousState === "object" && previousState !== null;
|
|
48
|
+
const shouldReplace = replace || !isNextObject || !isPrevObject;
|
|
49
|
+
if (shouldReplace) {
|
|
50
|
+
state = nextState;
|
|
51
|
+
} else {
|
|
52
|
+
const patch = nextState;
|
|
53
|
+
const prevObj = previousState;
|
|
54
|
+
let changed = false;
|
|
55
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
56
|
+
if (!Object.is(prevObj[key], value)) {
|
|
57
|
+
changed = true;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!changed) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
state = Object.assign({}, previousState, nextState);
|
|
65
|
+
}
|
|
66
|
+
const currentListeners = new Set(listeners);
|
|
67
|
+
currentListeners.forEach((listener) => listener(state, previousState));
|
|
68
|
+
};
|
|
69
|
+
const subscribe = (listener) => {
|
|
70
|
+
listeners.add(listener);
|
|
71
|
+
return () => listeners.delete(listener);
|
|
72
|
+
};
|
|
73
|
+
const destroy = () => {
|
|
74
|
+
listeners.clear();
|
|
75
|
+
};
|
|
76
|
+
return { getState, setState, subscribe, destroy };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/utils.ts
|
|
80
|
+
var shallow = (objA, objB) => {
|
|
81
|
+
if (Object.is(objA, objB)) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
if (typeof objA !== "object" || objA === null || typeof objB !== "object" || objB === null) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const keysA = Object.keys(objA);
|
|
88
|
+
const keysB = Object.keys(objB);
|
|
89
|
+
if (keysA.length !== keysB.length) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
for (let i = 0; i < keysA.length; i++) {
|
|
93
|
+
if (!Object.prototype.hasOwnProperty.call(objB, keysA[i]) || !Object.is(objA[keysA[i]], objB[keysA[i]])) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
};
|
|
99
|
+
function debounce(func, wait) {
|
|
100
|
+
let timeout;
|
|
101
|
+
let lastArgs;
|
|
102
|
+
let context;
|
|
103
|
+
const debounced = function(...args) {
|
|
104
|
+
context = this;
|
|
105
|
+
lastArgs = args;
|
|
106
|
+
clearTimeout(timeout);
|
|
107
|
+
timeout = setTimeout(() => {
|
|
108
|
+
func.apply(context, args);
|
|
109
|
+
lastArgs = void 0;
|
|
110
|
+
}, wait);
|
|
111
|
+
};
|
|
112
|
+
debounced.flush = () => {
|
|
113
|
+
if (timeout && lastArgs) {
|
|
114
|
+
clearTimeout(timeout);
|
|
115
|
+
func.apply(context, lastArgs);
|
|
116
|
+
lastArgs = void 0;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
debounced.cancel = () => {
|
|
120
|
+
clearTimeout(timeout);
|
|
121
|
+
lastArgs = void 0;
|
|
122
|
+
};
|
|
123
|
+
return debounced;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/createModel.ts
|
|
127
|
+
function createModel(initialState, options) {
|
|
128
|
+
const initialValue = typeof initialState === "function" ? initialState() : initialState;
|
|
129
|
+
const store = createStore(initialValue);
|
|
130
|
+
setupModelSideEffects(store, options);
|
|
131
|
+
const model = bindStore(store);
|
|
132
|
+
model.setState = store.setState;
|
|
133
|
+
model.getState = store.getState;
|
|
134
|
+
model.destroy = store.destroy;
|
|
135
|
+
model.reset = () => store.setState(initialValue, true);
|
|
136
|
+
return model;
|
|
137
|
+
}
|
|
138
|
+
function bindStore(store) {
|
|
139
|
+
const subscribe = (onStoreChange) => store.subscribe(() => onStoreChange());
|
|
140
|
+
const useModel = ((selector, equalityFn = shallow) => {
|
|
141
|
+
const selectorRef = react.useRef(selector);
|
|
142
|
+
selectorRef.current = selector;
|
|
143
|
+
const equalityRef = react.useRef(equalityFn);
|
|
144
|
+
equalityRef.current = equalityFn;
|
|
145
|
+
const hasSelectionRef = react.useRef(false);
|
|
146
|
+
const lastStateRef = react.useRef(null);
|
|
147
|
+
const lastSelectionRef = react.useRef(null);
|
|
148
|
+
const getSelection = () => {
|
|
149
|
+
const state = store.getState();
|
|
150
|
+
const selectorFn = selectorRef.current;
|
|
151
|
+
const nextSelection = selectorFn ? selectorFn(state) : state;
|
|
152
|
+
if (hasSelectionRef.current) {
|
|
153
|
+
if (Object.is(state, lastStateRef.current)) {
|
|
154
|
+
return lastSelectionRef.current;
|
|
155
|
+
}
|
|
156
|
+
if (equalityRef.current(lastSelectionRef.current, nextSelection)) {
|
|
157
|
+
lastStateRef.current = state;
|
|
158
|
+
return lastSelectionRef.current;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
hasSelectionRef.current = true;
|
|
162
|
+
lastStateRef.current = state;
|
|
163
|
+
lastSelectionRef.current = nextSelection;
|
|
164
|
+
return nextSelection;
|
|
165
|
+
};
|
|
166
|
+
return react.useSyncExternalStore(subscribe, getSelection, getSelection);
|
|
167
|
+
});
|
|
168
|
+
Object.defineProperty(useModel, "data", {
|
|
169
|
+
get: () => store.getState()
|
|
170
|
+
});
|
|
171
|
+
useModel.getState = store.getState;
|
|
172
|
+
useModel.setState = store.setState;
|
|
173
|
+
useModel.destroy = store.destroy;
|
|
174
|
+
useModel.reset = () => {
|
|
175
|
+
};
|
|
176
|
+
useModel.useState = useModel;
|
|
177
|
+
useModel.use = useModel;
|
|
178
|
+
const baseSubscribe = store.subscribe;
|
|
179
|
+
useModel.subscribe = (arg1, arg2, arg3) => {
|
|
180
|
+
var _a;
|
|
181
|
+
if (typeof arg1 === "function" && typeof arg2 !== "function") {
|
|
182
|
+
return baseSubscribe(arg1);
|
|
183
|
+
}
|
|
184
|
+
const selector = arg1;
|
|
185
|
+
const listener = arg2;
|
|
186
|
+
const options = arg3 != null ? arg3 : {};
|
|
187
|
+
const equalityFn = (_a = options.equalityFn) != null ? _a : Object.is;
|
|
188
|
+
let prevSelected = selector(store.getState());
|
|
189
|
+
if (options.fireImmediately) {
|
|
190
|
+
listener(prevSelected, prevSelected);
|
|
191
|
+
}
|
|
192
|
+
return baseSubscribe((nextState, prevState) => {
|
|
193
|
+
const nextSelected = selector(nextState);
|
|
194
|
+
const prevSelectedFromPrevState = selector(prevState);
|
|
195
|
+
if (equalityFn(prevSelectedFromPrevState, nextSelected)) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
prevSelected = nextSelected;
|
|
199
|
+
listener(nextSelected, prevSelectedFromPrevState);
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
return useModel;
|
|
203
|
+
}
|
|
204
|
+
function setupModelSideEffects(store, options) {
|
|
205
|
+
var _a, _b, _c;
|
|
206
|
+
const { storage } = getConfig();
|
|
207
|
+
const persist = typeof (options == null ? void 0 : options.persist) === "string" ? { key: options.persist } : options == null ? void 0 : options.persist;
|
|
208
|
+
const persistKey = persist == null ? void 0 : persist.key;
|
|
209
|
+
const syncEnabled = !!(options == null ? void 0 : options.sync);
|
|
210
|
+
if (!persistKey) {
|
|
211
|
+
if (syncEnabled) {
|
|
212
|
+
console.warn(
|
|
213
|
+
"[react-native-hox] `sync` \u9700\u8981 `persist` \u63D0\u4F9B key \u624D\u80FD\u5EFA\u7ACB\u901A\u9053"
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const beforePersist = (_a = persist == null ? void 0 : persist.beforePersist) != null ? _a : ((s) => s);
|
|
219
|
+
const beforeRecover = (_b = persist == null ? void 0 : persist.beforeRecover) != null ? _b : ((v) => v);
|
|
220
|
+
const debounceMs = (_c = persist == null ? void 0 : persist.debounce) != null ? _c : 100;
|
|
221
|
+
if (!storage) {
|
|
222
|
+
console.warn(
|
|
223
|
+
"[react-native-hox] \u672A\u914D\u7F6E storage\uFF0Cpersist \u5C06\u4E0D\u4F1A\u751F\u6548\uFF1B\u8BF7\u5148\u8C03\u7528 setup({ storage })"
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
let channel;
|
|
227
|
+
const instanceId = Math.random().toString(36).slice(2);
|
|
228
|
+
let suppressBroadcast = false;
|
|
229
|
+
if (storage) {
|
|
230
|
+
Promise.resolve(storage.getItem(persistKey)).then((val) => {
|
|
231
|
+
if (!val) return;
|
|
232
|
+
try {
|
|
233
|
+
const parsed = JSON.parse(val);
|
|
234
|
+
const recovered = beforeRecover(parsed);
|
|
235
|
+
suppressBroadcast = true;
|
|
236
|
+
store.setState(recovered, true);
|
|
237
|
+
} catch (e) {
|
|
238
|
+
console.error("[react-native-hox] Failed to parse stored value:", e);
|
|
239
|
+
} finally {
|
|
240
|
+
suppressBroadcast = false;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (syncEnabled && typeof BroadcastChannel !== "undefined") {
|
|
245
|
+
channel = new BroadcastChannel(`hox-sync-${persistKey}`);
|
|
246
|
+
channel.onmessage = (event) => {
|
|
247
|
+
const payload = event.data;
|
|
248
|
+
const nextState = (payload == null ? void 0 : payload.__hox) ? payload.state : payload;
|
|
249
|
+
const source = (payload == null ? void 0 : payload.__hox) ? payload.source : void 0;
|
|
250
|
+
if (source && source === instanceId) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
suppressBroadcast = true;
|
|
254
|
+
store.setState(nextState, true);
|
|
255
|
+
suppressBroadcast = false;
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const save = debounce((state, s) => {
|
|
259
|
+
if (!s) return;
|
|
260
|
+
try {
|
|
261
|
+
const prepared = beforePersist(state);
|
|
262
|
+
s.setItem(persistKey, JSON.stringify(prepared));
|
|
263
|
+
} catch (e) {
|
|
264
|
+
console.error("[react-native-hox] Save failed:", e);
|
|
265
|
+
}
|
|
266
|
+
}, debounceMs);
|
|
267
|
+
store.subscribe((state) => {
|
|
268
|
+
save(state, storage);
|
|
269
|
+
if (channel && !suppressBroadcast) {
|
|
270
|
+
channel.postMessage({ __hox: true, source: instanceId, state });
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
exports.createModel = createModel;
|
|
276
|
+
exports.setup = setup;
|
|
277
|
+
exports.shallow = shallow;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { useRef, useSyncExternalStore } from 'react';
|
|
2
|
+
|
|
3
|
+
// src/createModel.ts
|
|
4
|
+
|
|
5
|
+
// src/config.ts
|
|
6
|
+
function getDefaultWebStorage() {
|
|
7
|
+
try {
|
|
8
|
+
const anyGlobal = globalThis;
|
|
9
|
+
const storage = anyGlobal == null ? void 0 : anyGlobal.localStorage;
|
|
10
|
+
if (!storage) {
|
|
11
|
+
return void 0;
|
|
12
|
+
}
|
|
13
|
+
const testKey = "__react_native_hox_test__";
|
|
14
|
+
storage.setItem(testKey, "1");
|
|
15
|
+
storage.removeItem(testKey);
|
|
16
|
+
return {
|
|
17
|
+
getItem: (key) => storage.getItem(key),
|
|
18
|
+
setItem: (key, value) => storage.setItem(key, value),
|
|
19
|
+
removeItem: (key) => storage.removeItem(key)
|
|
20
|
+
};
|
|
21
|
+
} catch {
|
|
22
|
+
return void 0;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
var config = {
|
|
26
|
+
storage: getDefaultWebStorage()
|
|
27
|
+
};
|
|
28
|
+
var setup = (options) => {
|
|
29
|
+
Object.assign(config, options);
|
|
30
|
+
};
|
|
31
|
+
var getConfig = () => config;
|
|
32
|
+
|
|
33
|
+
// src/vanilla.ts
|
|
34
|
+
var createStore = (initialState) => {
|
|
35
|
+
let state = initialState;
|
|
36
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
37
|
+
const getState = () => state;
|
|
38
|
+
const setState = (partial, replace = false) => {
|
|
39
|
+
const nextState = typeof partial === "function" ? partial(state) : partial;
|
|
40
|
+
if (Object.is(nextState, state)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const previousState = state;
|
|
44
|
+
const isNextObject = typeof nextState === "object" && nextState !== null;
|
|
45
|
+
const isPrevObject = typeof previousState === "object" && previousState !== null;
|
|
46
|
+
const shouldReplace = replace || !isNextObject || !isPrevObject;
|
|
47
|
+
if (shouldReplace) {
|
|
48
|
+
state = nextState;
|
|
49
|
+
} else {
|
|
50
|
+
const patch = nextState;
|
|
51
|
+
const prevObj = previousState;
|
|
52
|
+
let changed = false;
|
|
53
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
54
|
+
if (!Object.is(prevObj[key], value)) {
|
|
55
|
+
changed = true;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!changed) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
state = Object.assign({}, previousState, nextState);
|
|
63
|
+
}
|
|
64
|
+
const currentListeners = new Set(listeners);
|
|
65
|
+
currentListeners.forEach((listener) => listener(state, previousState));
|
|
66
|
+
};
|
|
67
|
+
const subscribe = (listener) => {
|
|
68
|
+
listeners.add(listener);
|
|
69
|
+
return () => listeners.delete(listener);
|
|
70
|
+
};
|
|
71
|
+
const destroy = () => {
|
|
72
|
+
listeners.clear();
|
|
73
|
+
};
|
|
74
|
+
return { getState, setState, subscribe, destroy };
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/utils.ts
|
|
78
|
+
var shallow = (objA, objB) => {
|
|
79
|
+
if (Object.is(objA, objB)) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (typeof objA !== "object" || objA === null || typeof objB !== "object" || objB === null) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const keysA = Object.keys(objA);
|
|
86
|
+
const keysB = Object.keys(objB);
|
|
87
|
+
if (keysA.length !== keysB.length) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
for (let i = 0; i < keysA.length; i++) {
|
|
91
|
+
if (!Object.prototype.hasOwnProperty.call(objB, keysA[i]) || !Object.is(objA[keysA[i]], objB[keysA[i]])) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
};
|
|
97
|
+
function debounce(func, wait) {
|
|
98
|
+
let timeout;
|
|
99
|
+
let lastArgs;
|
|
100
|
+
let context;
|
|
101
|
+
const debounced = function(...args) {
|
|
102
|
+
context = this;
|
|
103
|
+
lastArgs = args;
|
|
104
|
+
clearTimeout(timeout);
|
|
105
|
+
timeout = setTimeout(() => {
|
|
106
|
+
func.apply(context, args);
|
|
107
|
+
lastArgs = void 0;
|
|
108
|
+
}, wait);
|
|
109
|
+
};
|
|
110
|
+
debounced.flush = () => {
|
|
111
|
+
if (timeout && lastArgs) {
|
|
112
|
+
clearTimeout(timeout);
|
|
113
|
+
func.apply(context, lastArgs);
|
|
114
|
+
lastArgs = void 0;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
debounced.cancel = () => {
|
|
118
|
+
clearTimeout(timeout);
|
|
119
|
+
lastArgs = void 0;
|
|
120
|
+
};
|
|
121
|
+
return debounced;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/createModel.ts
|
|
125
|
+
function createModel(initialState, options) {
|
|
126
|
+
const initialValue = typeof initialState === "function" ? initialState() : initialState;
|
|
127
|
+
const store = createStore(initialValue);
|
|
128
|
+
setupModelSideEffects(store, options);
|
|
129
|
+
const model = bindStore(store);
|
|
130
|
+
model.setState = store.setState;
|
|
131
|
+
model.getState = store.getState;
|
|
132
|
+
model.destroy = store.destroy;
|
|
133
|
+
model.reset = () => store.setState(initialValue, true);
|
|
134
|
+
return model;
|
|
135
|
+
}
|
|
136
|
+
function bindStore(store) {
|
|
137
|
+
const subscribe = (onStoreChange) => store.subscribe(() => onStoreChange());
|
|
138
|
+
const useModel = ((selector, equalityFn = shallow) => {
|
|
139
|
+
const selectorRef = useRef(selector);
|
|
140
|
+
selectorRef.current = selector;
|
|
141
|
+
const equalityRef = useRef(equalityFn);
|
|
142
|
+
equalityRef.current = equalityFn;
|
|
143
|
+
const hasSelectionRef = useRef(false);
|
|
144
|
+
const lastStateRef = useRef(null);
|
|
145
|
+
const lastSelectionRef = useRef(null);
|
|
146
|
+
const getSelection = () => {
|
|
147
|
+
const state = store.getState();
|
|
148
|
+
const selectorFn = selectorRef.current;
|
|
149
|
+
const nextSelection = selectorFn ? selectorFn(state) : state;
|
|
150
|
+
if (hasSelectionRef.current) {
|
|
151
|
+
if (Object.is(state, lastStateRef.current)) {
|
|
152
|
+
return lastSelectionRef.current;
|
|
153
|
+
}
|
|
154
|
+
if (equalityRef.current(lastSelectionRef.current, nextSelection)) {
|
|
155
|
+
lastStateRef.current = state;
|
|
156
|
+
return lastSelectionRef.current;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
hasSelectionRef.current = true;
|
|
160
|
+
lastStateRef.current = state;
|
|
161
|
+
lastSelectionRef.current = nextSelection;
|
|
162
|
+
return nextSelection;
|
|
163
|
+
};
|
|
164
|
+
return useSyncExternalStore(subscribe, getSelection, getSelection);
|
|
165
|
+
});
|
|
166
|
+
Object.defineProperty(useModel, "data", {
|
|
167
|
+
get: () => store.getState()
|
|
168
|
+
});
|
|
169
|
+
useModel.getState = store.getState;
|
|
170
|
+
useModel.setState = store.setState;
|
|
171
|
+
useModel.destroy = store.destroy;
|
|
172
|
+
useModel.reset = () => {
|
|
173
|
+
};
|
|
174
|
+
useModel.useState = useModel;
|
|
175
|
+
useModel.use = useModel;
|
|
176
|
+
const baseSubscribe = store.subscribe;
|
|
177
|
+
useModel.subscribe = (arg1, arg2, arg3) => {
|
|
178
|
+
var _a;
|
|
179
|
+
if (typeof arg1 === "function" && typeof arg2 !== "function") {
|
|
180
|
+
return baseSubscribe(arg1);
|
|
181
|
+
}
|
|
182
|
+
const selector = arg1;
|
|
183
|
+
const listener = arg2;
|
|
184
|
+
const options = arg3 != null ? arg3 : {};
|
|
185
|
+
const equalityFn = (_a = options.equalityFn) != null ? _a : Object.is;
|
|
186
|
+
let prevSelected = selector(store.getState());
|
|
187
|
+
if (options.fireImmediately) {
|
|
188
|
+
listener(prevSelected, prevSelected);
|
|
189
|
+
}
|
|
190
|
+
return baseSubscribe((nextState, prevState) => {
|
|
191
|
+
const nextSelected = selector(nextState);
|
|
192
|
+
const prevSelectedFromPrevState = selector(prevState);
|
|
193
|
+
if (equalityFn(prevSelectedFromPrevState, nextSelected)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
prevSelected = nextSelected;
|
|
197
|
+
listener(nextSelected, prevSelectedFromPrevState);
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
return useModel;
|
|
201
|
+
}
|
|
202
|
+
function setupModelSideEffects(store, options) {
|
|
203
|
+
var _a, _b, _c;
|
|
204
|
+
const { storage } = getConfig();
|
|
205
|
+
const persist = typeof (options == null ? void 0 : options.persist) === "string" ? { key: options.persist } : options == null ? void 0 : options.persist;
|
|
206
|
+
const persistKey = persist == null ? void 0 : persist.key;
|
|
207
|
+
const syncEnabled = !!(options == null ? void 0 : options.sync);
|
|
208
|
+
if (!persistKey) {
|
|
209
|
+
if (syncEnabled) {
|
|
210
|
+
console.warn(
|
|
211
|
+
"[react-native-hox] `sync` \u9700\u8981 `persist` \u63D0\u4F9B key \u624D\u80FD\u5EFA\u7ACB\u901A\u9053"
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const beforePersist = (_a = persist == null ? void 0 : persist.beforePersist) != null ? _a : ((s) => s);
|
|
217
|
+
const beforeRecover = (_b = persist == null ? void 0 : persist.beforeRecover) != null ? _b : ((v) => v);
|
|
218
|
+
const debounceMs = (_c = persist == null ? void 0 : persist.debounce) != null ? _c : 100;
|
|
219
|
+
if (!storage) {
|
|
220
|
+
console.warn(
|
|
221
|
+
"[react-native-hox] \u672A\u914D\u7F6E storage\uFF0Cpersist \u5C06\u4E0D\u4F1A\u751F\u6548\uFF1B\u8BF7\u5148\u8C03\u7528 setup({ storage })"
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
let channel;
|
|
225
|
+
const instanceId = Math.random().toString(36).slice(2);
|
|
226
|
+
let suppressBroadcast = false;
|
|
227
|
+
if (storage) {
|
|
228
|
+
Promise.resolve(storage.getItem(persistKey)).then((val) => {
|
|
229
|
+
if (!val) return;
|
|
230
|
+
try {
|
|
231
|
+
const parsed = JSON.parse(val);
|
|
232
|
+
const recovered = beforeRecover(parsed);
|
|
233
|
+
suppressBroadcast = true;
|
|
234
|
+
store.setState(recovered, true);
|
|
235
|
+
} catch (e) {
|
|
236
|
+
console.error("[react-native-hox] Failed to parse stored value:", e);
|
|
237
|
+
} finally {
|
|
238
|
+
suppressBroadcast = false;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (syncEnabled && typeof BroadcastChannel !== "undefined") {
|
|
243
|
+
channel = new BroadcastChannel(`hox-sync-${persistKey}`);
|
|
244
|
+
channel.onmessage = (event) => {
|
|
245
|
+
const payload = event.data;
|
|
246
|
+
const nextState = (payload == null ? void 0 : payload.__hox) ? payload.state : payload;
|
|
247
|
+
const source = (payload == null ? void 0 : payload.__hox) ? payload.source : void 0;
|
|
248
|
+
if (source && source === instanceId) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
suppressBroadcast = true;
|
|
252
|
+
store.setState(nextState, true);
|
|
253
|
+
suppressBroadcast = false;
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const save = debounce((state, s) => {
|
|
257
|
+
if (!s) return;
|
|
258
|
+
try {
|
|
259
|
+
const prepared = beforePersist(state);
|
|
260
|
+
s.setItem(persistKey, JSON.stringify(prepared));
|
|
261
|
+
} catch (e) {
|
|
262
|
+
console.error("[react-native-hox] Save failed:", e);
|
|
263
|
+
}
|
|
264
|
+
}, debounceMs);
|
|
265
|
+
store.subscribe((state) => {
|
|
266
|
+
save(state, storage);
|
|
267
|
+
if (channel && !suppressBroadcast) {
|
|
268
|
+
channel.postMessage({ __hox: true, source: instanceId, state });
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export { createModel, setup, shallow };
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-hox",
|
|
3
|
+
"version": "0.0.1-beta",
|
|
4
|
+
"description": "一个轻量级、类型安全、零心智负担的 React Native 状态管理解决方案。",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://gitee.com/ws18250840411/react-native-hox.git"
|
|
8
|
+
},
|
|
9
|
+
"license": "ISC",
|
|
10
|
+
"author": "wangws",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"module": "dist/index.mjs",
|
|
14
|
+
"react-native": "dist/index.js",
|
|
15
|
+
"types": "dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.mjs",
|
|
20
|
+
"require": "./dist/index.js",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"tag": "beta"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "jest",
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"prepack": "npm run build"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"react": ">=16.8",
|
|
39
|
+
"react-native": ">=0.64"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/jest": "^30.0.0",
|
|
43
|
+
"@types/react": "^19.2.8",
|
|
44
|
+
"@types/react-native": "^0.72.8",
|
|
45
|
+
"@types/react-test-renderer": "^19.1.0",
|
|
46
|
+
"jest": "^30.2.0",
|
|
47
|
+
"react": "^19.2.0",
|
|
48
|
+
"react-test-renderer": "^19.2.3",
|
|
49
|
+
"ts-jest": "^29.4.6",
|
|
50
|
+
"tsup": "^8.5.0",
|
|
51
|
+
"typescript": "^5.9.3"
|
|
52
|
+
}
|
|
53
|
+
}
|