Repository: alibaba/hooks
Pull request #826 (2021-01-11 15:10:42)

feat: 添加 useSafeState hook
避免组件卸载后，异步回调中执行 setState


Patch:
diff --git a/packages/hooks/src/useSafeState/demo/demo1.tsx b/packages/hooks/src/useSafeState/demo/demo1.tsx
new file mode 100644
index 00000000..cd4b3fe6
--- /dev/null
+++ b/packages/hooks/src/useSafeState/demo/demo1.tsx
@@ -0,0 +1,40 @@
+/**
+ * title: Default usage
+ *
+ * title.zh-CN: 基础用法
+ */
+
+import React, { useState } from 'react';
+import useSafeState from '../index';
+
+
+const Child = () => {
+
+  const [value, setValue] = useSafeState<string>()
+  React.useEffect(() => {
+    setTimeout(() => {
+      setValue('从服务端获取的数据')
+    }, 5000)
+  }, [])
+
+  const text = value || '正在获取数据。。。'
+
+  return (
+    <div>{text}</div>
+  )
+}
+
+
+export default () => {
+  
+
+  const [visible, setVisible] = useSafeState(true);
+
+
+  return (
+    <div>
+      <button onClick={() => setVisible(false)}>卸载</button>
+      {visible && <Child />}
+    </div>
+  );
+};
diff --git a/packages/hooks/src/useSafeState/index.en-US.md b/packages/hooks/src/useSafeState/index.en-US.md
new file mode 100644
index 00000000..a648a970
--- /dev/null
+++ b/packages/hooks/src/useSafeState/index.en-US.md
@@ -0,0 +1,26 @@
+---
+title: useSafeState
+nav:
+  title: Hooks
+  path: /hooks
+group:
+  title: State
+  path: /state
+---
+
+# useSafeState
+It is exactly the same with React.useState , but after the component is unloaded, the setstate in the asynchronous callback is no longer executed to avoid memory leakage caused by updating the state after the component is unloaded
+
+
+
+## Examples
+
+
+<code src="./demo/demo1.tsx" />
+
+## API
+
+```typescript
+const [state, setState] = useSafeState(initialState)
+```
+
diff --git a/packages/hooks/src/useSafeState/index.ts b/packages/hooks/src/useSafeState/index.ts
new file mode 100644
index 00000000..c76d358e
--- /dev/null
+++ b/packages/hooks/src/useSafeState/index.ts
@@ -0,0 +1,22 @@
+
+import React, { Dispatch, SetStateAction } from 'react'
+import useUnmountedRef from '../useUnmountedRef'
+
+function useSafeState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>]
+
+function useSafeState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];
+
+
+function useSafeState(initialState?) {
+  const unmountedRef = useUnmountedRef()
+  const [state, setState] = React.useState(initialState)
+  const setCurrentState = (currentState) => {
+    /** 如果组件已经卸载则不再更新 state */
+    if (unmountedRef.current) return
+    setState(currentState)
+  }
+
+  return [state, setCurrentState] as const
+}
+
+export default useSafeState
diff --git a/packages/hooks/src/useSafeState/index.zh-CN.md b/packages/hooks/src/useSafeState/index.zh-CN.md
new file mode 100644
index 00000000..3824577b
--- /dev/null
+++ b/packages/hooks/src/useSafeState/index.zh-CN.md
@@ -0,0 +1,25 @@
+---
+title: useSafeState
+nav:
+  title: Hooks
+  path: /hooks
+group:
+  title: State
+  path: /state
+---
+
+# useSafeState
+用法与 React.useState 完全一样，但是在组件卸载后异步回调内的 setState 不再执行，避免因组件卸载后更新状态而导致的内存泄漏
+
+## 代码演示
+
+### 基础用法，与 React.useState 完全一样
+
+<code src="./demo/demo1.tsx" />
+
+## API
+
+```typescript
+const [state, setState] = useSafeState(initialState)
+```
+


Test patch:
diff --git a/packages/hooks/src/useSafeState/__tests__/index.test.ts b/packages/hooks/src/useSafeState/__tests__/index.test.ts
new file mode 100644
index 00000000..674773e2
--- /dev/null
+++ b/packages/hooks/src/useSafeState/__tests__/index.test.ts
@@ -0,0 +1,41 @@
+import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks';
+import useSafeState from '../index';
+
+describe('useSetState', () => {
+  it('should be defined', () => {
+    expect(useSafeState).toBeDefined();
+  });
+
+  const setUp = (initialValue: any) =>
+    renderHook(() => {
+      const [state, setState] = useSafeState(initialValue);
+      return {
+        state,
+        setState,
+      } as const;
+    });
+
+  it('should support initialValue', () => {
+    const hook = setUp({
+      hello: 'world',
+    });
+    expect(hook.result.current.state).toEqual({ hello: 'world' });
+  });
+
+  it('should support update', () => {
+    const hook = setUp(0);
+    act(() => {
+      hook.result.current.setState(5);
+    });
+    expect(hook.result.current.state).toEqual(5);
+  });
+
+  it('should not support update when unmount', () => {
+    const hook = setUp(0);
+    hook.unmount();
+    act(() => {
+      hook.result.current.setState(5);
+    });
+    expect(hook.result.current.state).toEqual(0);
+  });
+});