diff --git a/CHANGELOG.md b/CHANGELOG.md
index a72e456..3f8236f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## v3
+
+- 删除hooks `useDefined`
+
## [2.0.1](https://github.com/foca-js/foca/compare/v2.0.0...v2.0.1) (2023-08-10)
- react-redux 版本从 8.1.1 升级到 8.1.2 (#40)
diff --git a/docs/advanced.md b/docs/advanced.md
index a6f879f..fd91f25 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -37,43 +37,6 @@ const user3Model = cloneModel('users3', userModel, (prev) => {
});
```
-# 局部模型
-
-通过`defineModel`和`cloneModel`创建的模型均为全局类别的模型,数据一直保持在内存中,直到应用关闭或者退出才会释放,对于比较大的项目,这可能会有性能问题。所以有时候你其实想要一种`用完就扔`的模型,即在 React 组件初始化时把模型数据扔到 store 中,当 React 组件被销毁时,模型的数据也跟着销毁。现在局部模型很适合你的需求:
-
-```tsx
-import { useEffect } from 'react';
-import { defineModel, useDefined } from 'foca';
-
-// test.model.ts
-export const testModel = defineModel('test', {
- initialState: { count: 0 },
- reducers: {
- plus(state, value: number) {
- state.count += value;
- },
- },
-});
-
-// App.tsx
-const App: FC = () => {
- const model = useDefined(testModel);
- const { count } = useModel(model);
-
- useEffect(() => {
- model.plus(1);
- }, []);
-
- return
{count}
;
-};
-```
-
-利用 `useDefined` 函数根据全局模型创建一个新的局部模型,然后就是通用的模型操作,这似乎没有增加工作量(因为只多了一行)。下面我列举了局部函数的几个特点:
-
-- 组件内部使用,不污染全局
-- 数据随组件自动挂载/释放
-- 有效降低内存占用量
-
# loadings
默认地,methods 函数只会保存一份执行状态,如果你在同一时间多次执行同一个函数,那么状态就会互相覆盖,产生错乱的数据。如果现在有 10 个按钮,点击每个按钮都会执行`model.methodX(id)`,那么我们如何知道是哪个按钮执行的呢?这时候我们需要为执行状态开辟一个独立的存储空间,让同一个函数拥有多个状态互不干扰。
diff --git a/docs/api.md b/docs/api.md
index 2968fba..9a8a943 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -1,12 +1,11 @@
-| 方法 | 描述 | 使用频率 | 指南 |
-| ----------------- | --------------------------------------- | ----------------------------------- | ------------------------------------- |
-| **store.init** | 初始化仓库 | :star2: | [开始使用](/initialize?id=仓库) |
-| **store.refresh** | 重置仓库数据 | :star2: | [进阶用法](/advanced?id=重置所有数据) |
-| **defineModel** | 创建模型 | :star2::star2::star2::star2::star2: | [模型](/model?id=model) |
-| **cloneModel** | 复制模型并允许小量修改 | :star2: | [进阶用法](/advanced?id=克隆模型) |
-| **useDefined** | 在 hooks 中创建局部模型,数据随组件释放 | :star2::star2::star2: | [进阶用法](/advanced?id=局部模型) |
-| **useModel** | 在 hooks 中使用模型的状态 | :star2::star2::star2::star2::star2: | [数据对接](/react?id=usemodel) |
-| **useComputed** | 在 hooks 中使用计算属性 | :star2::star2::star2: | [数据对接](/react?id=usecomputed) |
-| **useLoading** | 在 hooks 中获取异步函数的执行状态 | :star2::star2::star2::star2::star2: | [数据对接](/react?id=useloading) |
-| **getLoading** | 获取异步函数的当前执行状态 | :star2: | [通用属性](/model?id=loading) |
-| **connect** | 在 class 组件中连接 react 和 redux | :star2: | [数据对接](/react?id=connect) |
+| 方法 | 描述 | 使用频率 | 指南 |
+| ----------------- | ---------------------------------- | ----------------------------------- | ------------------------------------- |
+| **store.init** | 初始化仓库 | :star2: | [开始使用](/initialize?id=仓库) |
+| **store.refresh** | 重置仓库数据 | :star2: | [进阶用法](/advanced?id=重置所有数据) |
+| **defineModel** | 创建模型 | :star2::star2::star2::star2::star2: | [模型](/model?id=model) |
+| **cloneModel** | 复制模型并允许小量修改 | :star2: | [进阶用法](/advanced?id=克隆模型) |
+| **useModel** | 在 hooks 中使用模型的状态 | :star2::star2::star2::star2::star2: | [数据对接](/react?id=usemodel) |
+| **useComputed** | 在 hooks 中使用计算属性 | :star2::star2::star2: | [数据对接](/react?id=usecomputed) |
+| **useLoading** | 在 hooks 中获取异步函数的执行状态 | :star2::star2::star2::star2::star2: | [数据对接](/react?id=useloading) |
+| **getLoading** | 获取异步函数的当前执行状态 | :star2: | [通用属性](/model?id=loading) |
+| **connect** | 在 class 组件中连接 react 和 redux | :star2: | [数据对接](/react?id=connect) |
diff --git a/docs/events.md b/docs/events.md
index 75fee9d..c57073d 100644
--- a/docs/events.md
+++ b/docs/events.md
@@ -63,24 +63,3 @@ export const testModel = defineModel('test', {
},
});
```
-
-## onDestroy
-
-模型数据从 store 卸载时的回调通知。onDestroy 事件只针对`局部模型`,即通过`useDefined`这个 hooks api 创建的模型才会触发,因为局部模型是跟随组件一起创建和销毁的。
-
-注意,当触发 onDestroy 回调时,模型已经被卸载了,所以无法再拿到当前数据,而且`this`上下文也被限制使用了。
-
-```typescript
-import { defineModel } from 'foca';
-
-const initialState = { count: 0 };
-
-export const testModel = defineModel('test', {
- initialState,
- events: {
- onDestroy() {
- console.log('Destroyed');
- },
- },
-});
-```
diff --git a/docs/mindMap.svg b/docs/mindMap.svg
index 6a2b7ba..05533ae 100644
--- a/docs/mindMap.svg
+++ b/docs/mindMap.svg
@@ -1,4 +1,4 @@
-
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index be0a6b2..cc8a214 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -8,7 +8,6 @@ export { useLoading } from './api/useLoading';
export { getLoading } from './api/getLoading';
export { connect } from './redux/connect';
export { useComputed } from './reactive/useComputed';
-export { useDefined } from './model/useDefined';
// 入口使用
export { compose } from 'redux';
diff --git a/src/model/defineModel.ts b/src/model/defineModel.ts
index 512869c..84ecb27 100644
--- a/src/model/defineModel.ts
+++ b/src/model/defineModel.ts
@@ -206,7 +206,7 @@ export const defineModel = <
}
if (events) {
- const { onInit, onChange, onDestroy } = events;
+ const { onInit, onChange } = events;
const eventCtx: EventCtx = Object.assign(
composeGetter({ name: uniqueName }, getState),
enhancedMethods.external,
@@ -233,19 +233,6 @@ export const defineModel = <
);
}
- if (onDestroy) {
- subscriptions.push(
- modelStore.subscribe(() => {
- if (eventCtx.state === void 0) {
- for (let i = 0; i < subscriptions.length; ++i) {
- subscriptions[i]!();
- }
- onDestroy.call(null as never);
- }
- }),
- );
- }
-
if (onInit) {
/**
* 初始化时,用到它的React组件可能还没加载,所以执行async-method时无法判断是否需要保存loading。因此需要一个钩子来处理事件周期
diff --git a/src/model/useDefined.ts b/src/model/useDefined.ts
deleted file mode 100644
index f4e8c15..0000000
--- a/src/model/useDefined.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { useEffect, useMemo, useState } from 'react';
-import { DestroyLodingAction, DESTROY_LOADING } from '../actions/loading';
-import { loadingStore } from '../store/loadingStore';
-import { ModelStore, modelStore } from '../store/modelStore';
-import { cloneModel } from './cloneModel';
-import { HookModel as HookModel, Model } from './types';
-
-let nameCounter = 0;
-const hotReloadCounter: Record = {};
-
-export const useDefined = <
- State extends object = object,
- Action extends object = object,
- Effect extends object = object,
- Computed extends object = object,
->(
- globalModel: Model,
-): HookModel => {
- const modelName = globalModel.name;
- const initialCount = useState(() => nameCounter++)[0];
-
- const uniqueName =
- process.env.NODE_ENV === 'production'
- ? useProdName(modelName, initialCount)
- : useDevName(modelName, initialCount, new Error());
-
- const hookModel = useMemo(() => {
- return cloneModel(uniqueName, globalModel);
- }, [uniqueName]);
-
- return hookModel as any;
-};
-
-const useProdName = (modelName: string, count: number) => {
- const uniqueName = modelName + '#' + count;
-
- useEffect(
- () => () => {
- setTimeout(unmountModel, 0, uniqueName);
- },
- [uniqueName],
- );
-
- return uniqueName;
-};
-
-/**
- * 开发模式下,需要Hot Reload。
- * 必须保证数据不会丢,即如果用户一直保持`model.name`不变,就被判定为可以共享热更新之前的数据。
- *
- * 必须严格控制count在组件内的自增次数,否则在第一次修改model的name时,总是会报错:
- * Warning: Cannot update a component (`XXX`) while rendering a different component (`XXX`)
- */
-const useDevName = (modelName: string, count: number, err: Error) => {
- const componentName = useMemo((): string => {
- try {
- const stacks = err.stack!.split('\n');
-
- const innerNamePattern = new RegExp(
- // vitest测试框架的stack增加了 Module.
- `at\\s(?:Module\\.)?${useDefined.name}\\s\\(`,
- 'i',
- );
- const componentNamePattern = /at\s(.+?)\s\(/i;
-
- for (let i = 0; i < stacks.length; ++i) {
- if (innerNamePattern.test(stacks[i]!)) {
- return stacks[i + 1]!.match(componentNamePattern)![1]!;
- }
- }
- } catch {}
-
- return 'Anonymous';
- }, [err.stack]);
-
- const uniqueName = `${componentName}:${count}:${modelName}`;
-
- useMemo(() => {
- hotReloadCounter[uniqueName] ||= 0;
- ++hotReloadCounter[uniqueName];
- }, [uniqueName]);
-
- useEffect(() => {
- const prev = hotReloadCounter[uniqueName];
- return () => {
- /**
- * 热更新时会重新执行一次useEffect
- * setTimeout可以让其他useEffect有充分的时间使用model
- *
- * 需要卸载模型的场景是:
- * 1. 组件hooks增减或者调换顺序(initialCount会自增)
- * 2. 组件卸载
- * 3. model.name变更
- */
- setTimeout(() => {
- const active = prev !== hotReloadCounter[uniqueName];
- active || unmountModel(uniqueName);
- });
- };
- }, [uniqueName]);
-
- return uniqueName;
-};
-
-const unmountModel = (modelName: string) => {
- ModelStore.removeReducer.call(modelStore, modelName);
- loadingStore.dispatch({
- type: DESTROY_LOADING,
- model: modelName,
- });
-};
diff --git a/test/lifecycle.test.ts b/test/lifecycle.test.ts
index 97b41af..9eee4a1 100644
--- a/test/lifecycle.test.ts
+++ b/test/lifecycle.test.ts
@@ -1,7 +1,6 @@
import sleep from 'sleep-promise';
import { cloneModel, defineModel, engines, store } from '../src';
import { PersistSchema } from '../src/persist/PersistItem';
-import { ModelStore } from '../src/store/modelStore';
describe('onInit', () => {
afterEach(() => {
@@ -188,63 +187,3 @@ describe('onChange', () => {
);
});
});
-
-describe('onDestroy', () => {
- beforeEach(() => {
- store.init();
- });
-
- afterEach(() => {
- store.unmount();
- });
-
- test('call onDestroy when invoke store.destroy()', async () => {
- const spy = vitest.fn();
- const model = defineModel('events' + Math.random(), {
- initialState: { count: 0 },
- reducers: {
- update(state) {
- state.count += 1;
- },
- },
- events: {
- onDestroy: spy,
- },
- });
-
- await store.onInitialized();
-
- model.update();
- expect(spy).toBeCalledTimes(0);
- ModelStore.removeReducer.call(store, model.name);
- expect(spy).toBeCalledTimes(1);
- spy.mockRestore();
- });
-
- test('should not call onChange', async () => {
- const destroySpy = vitest.fn();
- const changeSpy = vitest.fn();
- const model = defineModel('events' + Math.random(), {
- initialState: { count: 0 },
- reducers: {
- update(state) {
- state.count += 1;
- },
- },
- events: {
- onChange: changeSpy,
- onDestroy: destroySpy,
- },
- });
-
- await store.onInitialized();
-
- model.update();
- expect(destroySpy).toBeCalledTimes(0);
- expect(changeSpy).toBeCalledTimes(1);
- ModelStore.removeReducer.call(store, model.name);
- expect(destroySpy).toBeCalledTimes(1);
- expect(changeSpy).toBeCalledTimes(1);
- destroySpy.mockRestore();
- });
-});
diff --git a/test/typescript/useDefined.check.ts b/test/typescript/useDefined.check.ts
deleted file mode 100644
index 59baecb..0000000
--- a/test/typescript/useDefined.check.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { defineModel, useDefined, useLoading, useModel } from '../../src';
-import { basicModel } from '../models/basicModel';
-
-const hookModel = useDefined(basicModel);
-
-useModel(hookModel);
-useModel(hookModel, (state) => state.count);
-useLoading(hookModel.pureAsync);
-useLoading(hookModel.pureAsync.room);
-
-// @ts-expect-error
-useModel(basicModel, hookModel);
-// @ts-expect-error
-useModel(hookModel, basicModel);
-// @ts-expect-error
-useModel(hookModel, basicModel, () => {});
-
-// @ts-expect-error
-useDefined(hookModel);
-// @ts-expect-error
-cloneModel(hookModel);
-
-defineModel('local-demo-1', {
- initialState: {},
- events: {
- onDestroy() {
- // @ts-expect-error
- this.anything;
- },
- },
-});
diff --git a/test/useDefined.test.tsx b/test/useDefined.test.tsx
deleted file mode 100644
index 546a27a..0000000
--- a/test/useDefined.test.tsx
+++ /dev/null
@@ -1,150 +0,0 @@
-import { act, cleanup, render } from '@testing-library/react';
-import { useEffect, useState } from 'react';
-import sleep from 'sleep-promise';
-import {
- defineModel,
- FocaProvider,
- store,
- useLoading,
- useDefined,
- Model,
- HookModel,
-} from '../src';
-import { loadingStore } from '../src/store/loadingStore';
-import { renderHook } from './helpers/renderHook';
-import { basicModel } from './models/basicModel';
-
-(['development', 'production'] as const).forEach((env) => {
- describe(`[${env} mode]`, () => {
- beforeEach(() => {
- store.init();
- process.env.NODE_ENV = env;
- });
-
- afterEach(async () => {
- process.env.NODE_ENV = 'testing';
- cleanup();
- await sleep(10);
- store.unmount();
- });
-
- test('can register to modelStore and remove from modelStore', async () => {
- const { result, unmount } = renderHook(() => useDefined(basicModel));
-
- expect(result.current).not.toBe(basicModel);
- expect(store.getState()).toHaveProperty(
- result.current.name,
- result.current.state,
- );
-
- unmount();
- await sleep(1);
- expect(store.getState()).not.toHaveProperty(result.current.name);
- });
-
- test('can register to loadingStore and remove from loadingStore', async () => {
- const { result, unmount } = renderHook(() => {
- const model = useDefined(basicModel);
- useLoading(basicModel.pureAsync);
- useLoading(model.pureAsync);
-
- return model;
- });
-
- const key1 = `${result.current.name}.pureAsync`;
- const key2 = `${basicModel.name}.pureAsync`;
- expect(loadingStore.getState()).not.toHaveProperty(key1);
-
- await act(async () => {
- const promise1 = result.current.pureAsync();
- const promise2 = basicModel.pureAsync();
-
- expect(loadingStore.getState()).toHaveProperty(key1);
- expect(loadingStore.getState()).toHaveProperty(key2);
-
- await promise1;
- await promise2;
- });
-
- expect(loadingStore.getState()).toHaveProperty(key1);
- expect(loadingStore.getState()).toHaveProperty(key2);
-
- unmount();
- await sleep(1);
- expect(loadingStore.getState()).not.toHaveProperty(key1);
- expect(loadingStore.getState()).toHaveProperty(key2);
- });
-
- test('call onDestroy event when local model is destroyed', async () => {
- const spy = vitest.fn();
- const globalModel = defineModel('local-demo-1', {
- initialState: {},
- events: {
- onDestroy: spy,
- },
- });
-
- const { unmount } = renderHook(() => useDefined(globalModel));
-
- expect(spy).toBeCalledTimes(0);
- unmount();
- await sleep(1);
- expect(spy).toBeCalledTimes(1);
- basicModel.plus(1);
- expect(spy).toBeCalledTimes(1);
- });
-
- test('recreate hook model when global model changed', async () => {
- const globalModel = defineModel('hook-demo-2', {
- initialState: {},
- });
-
- const { result } = renderHook(() => {
- const [state, setState] = useState(basicModel);
-
- const model = useDefined(state);
-
- useEffect(() => {
- setTimeout(() => {
- setState(globalModel);
- }, 20);
- }, []);
-
- return model;
- });
-
- const name1 = result.current.name;
- expect(name1).toMatch(basicModel.name);
- expect(store.getState()).toHaveProperty(name1);
-
- await act(async () => {
- await sleep(30);
- });
-
- await sleep(10);
-
- expect(result.current.name).not.toBe('hook-demo-2');
- expect(result.current.name).toMatch('hook-demo-2');
- expect(store.getState()).not.toHaveProperty(name1);
- });
-
- test.runIf(env === 'development')(
- 'Can get component name in dev mode',
- () => {
- let model!: HookModel;
- function MyApp() {
- model = useDefined(basicModel);
- return null;
- }
-
- render(
-
-
- ,
- );
-
- expect(model.name).toMatch('MyApp:');
- },
- );
- });
-});