Repository: alibaba/hooks
Pull request #1057 (2021-07-22 07:42:30)

refactor: useLocalStorageState


Patch:
diff --git a/packages/hooks/src/createUseStorageState/index.ts b/packages/hooks/src/createUseStorageState/index.ts
index 65d10f87..92571a9e 100644
--- a/packages/hooks/src/createUseStorageState/index.ts
+++ b/packages/hooks/src/createUseStorageState/index.ts
@@ -1,4 +1,6 @@
-import { useState, useCallback } from 'react';
+/* eslint-disable no-empty */
+import { useState } from 'react';
+import useMemoizedFn from '../useMemoizedFn';
 import useUpdateEffect from '../useUpdateEffect';
 
 export interface IFuncUpdater<T> {
@@ -8,67 +10,84 @@ export interface IFuncStorage {
   (): Storage;
 }
 
+export interface Options<T> {
+  serializer?: (value: T) => string;
+  deserializer?: (value: string) => T;
+}
+
+export interface OptionsWithDefaultValue<T> extends Options<T> {
+  defaultValue: T | IFuncUpdater<T>;
+}
+
 export type StorageStateResult<T> = [T | undefined, (value?: T | IFuncUpdater<T>) => void];
-export type StorageStateResultHasDefaultValue<T> = [T, (value: T | IFuncUpdater<T>) => void];
+export type StorageStateResultHasDefaultValue<T> = [T, (value?: T | IFuncUpdater<T>) => void];
 
 function isFunction<T>(obj: any): obj is T {
   return typeof obj === 'function';
 }
 
-export function createUseStorageState(nullishStorage: Storage | null) {
-  function useStorageState<T = undefined>(key: string): StorageStateResult<T>;
+export function createUseStorageState(getStorage: () => Storage | undefined) {
+  function useStorageState<T = any>(key: string, options?: Options<T>): StorageStateResult<T>;
   function useStorageState<T>(
     key: string,
-    defaultValue: T | IFuncUpdater<T>,
+    options: OptionsWithDefaultValue<T>,
   ): StorageStateResultHasDefaultValue<T>;
 
-  function useStorageState<T>(key: string, defaultValue?: T | IFuncUpdater<T>) {
-    const storage = nullishStorage as Storage;
-    const [state, setState] = useState<T | undefined>(() => getStoredValue());
-    useUpdateEffect(() => {
-      setState(getStoredValue());
-    }, [key]);
+  function useStorageState<T>(key: string, options?: Options<T> & OptionsWithDefaultValue<T>) {
+    let storage: Storage | undefined;
+
+    // https://github.com/alibaba/hooks/issues/800
+    try {
+      storage = getStorage();
+    } catch (err) {
+      console.error(err);
+    }
+
+    const serializer = (value: T) => {
+      if (options?.serializer) {
+        return options?.serializer(value);
+      }
+      return JSON.stringify(value);
+    };
+
+    const deserializer = (value: string) => {
+      if (options?.deserializer) {
+        return options?.deserializer(value);
+      }
+      return JSON.parse(value);
+    };
 
     function getStoredValue() {
-      const raw = storage.getItem(key);
+      const raw = storage?.getItem(key);
       if (raw) {
-        try {
-          return JSON.parse(raw);
-        } catch (e) {}
+        return deserializer(raw);
       }
-      if (isFunction<IFuncUpdater<T>>(defaultValue)) {
-        return defaultValue();
+      if (isFunction<IFuncUpdater<T>>(options?.defaultValue)) {
+        return options?.defaultValue();
       }
-      return defaultValue;
+      return options?.defaultValue;
     }
 
-    const updateState = useCallback(
-      (value?: T | IFuncUpdater<T>) => {
-        if (typeof value === 'undefined') {
-          storage.removeItem(key);
-          setState(undefined);
-        } else if (isFunction<IFuncUpdater<T>>(value)) {
-          const previousState = getStoredValue();
-          const currentState = value(previousState);
-          storage.setItem(key, JSON.stringify(currentState));
-          setState(currentState);
-        } else {
-          storage.setItem(key, JSON.stringify(value));
-          setState(value);
-        }
-      },
-      [key],
-    );
+    const [state, setState] = useState<T | undefined>(() => getStoredValue());
+    useUpdateEffect(() => {
+      setState(getStoredValue());
+    }, [key]);
+
+    const updateState = (value?: T | IFuncUpdater<T>) => {
+      if (typeof value === 'undefined') {
+        setState(undefined);
+        storage?.removeItem(key);
+      } else if (isFunction<IFuncUpdater<T>>(value)) {
+        const currentState = value(state);
+        setState(currentState);
+        storage?.setItem(key, serializer(currentState));
+      } else {
+        setState(value);
+        storage?.setItem(key, serializer(value));
+      }
+    };
 
-    return [state, updateState];
-  }
-  if (!nullishStorage) {
-    return function (_: string, defaultValue: any) {
-      return [
-        isFunction<IFuncUpdater<any>>(defaultValue) ? defaultValue() : defaultValue,
-        () => {},
-      ];
-    } as typeof useStorageState;
+    return [state, useMemoizedFn(updateState)];
   }
   return useStorageState;
 }
diff --git a/packages/hooks/src/useLocalStorageState/demo/demo1.tsx b/packages/hooks/src/useLocalStorageState/demo/demo1.tsx
index 38e41b81..fbbda523 100644
--- a/packages/hooks/src/useLocalStorageState/demo/demo1.tsx
+++ b/packages/hooks/src/useLocalStorageState/demo/demo1.tsx
@@ -1,8 +1,8 @@
 /**
- * title: Persist state into localStorage
+ * title: Store state into localStorage
  * desc: Refresh this page and you will find the value of input box get restored from localStorage.
  *
- * title.zh-CN: 将 state 持久化在 localStorage 中
+ * title.zh-CN: 将 state 存储在 localStorage 中
  * desc.zh-CN: 刷新页面后，可以看到输入框中的内容被从 localStorage 中恢复了。
  */
 
@@ -10,7 +10,10 @@ import React from 'react';
 import { useLocalStorageState } from 'ahooks';
 
 export default function () {
-  const [message, setMessage] = useLocalStorageState('user-message', 'Hello~');
+  const [message, setMessage] = useLocalStorageState('use-local-storage-state-demo1', {
+    defaultValue: 'Hello~',
+  });
+
   return (
     <>
       <input
diff --git a/packages/hooks/src/useLocalStorageState/demo/demo2.tsx b/packages/hooks/src/useLocalStorageState/demo/demo2.tsx
index fab10848..df09a108 100644
--- a/packages/hooks/src/useLocalStorageState/demo/demo2.tsx
+++ b/packages/hooks/src/useLocalStorageState/demo/demo2.tsx
@@ -12,7 +12,9 @@ import { useLocalStorageState } from 'ahooks';
 const defaultArray = ['a', 'e', 'i', 'o', 'u'];
 
 export default function () {
-  const [value, setValue] = useLocalStorageState('cascader', defaultArray);
+  const [value, setValue] = useLocalStorageState('use-local-storage-state-demo2', {
+    defaultValue: defaultArray,
+  });
 
   return (
     <>
diff --git a/packages/hooks/src/useLocalStorageState/demo/demo3.tsx b/packages/hooks/src/useLocalStorageState/demo/demo3.tsx
index 63c71a52..51a226a9 100644
--- a/packages/hooks/src/useLocalStorageState/demo/demo3.tsx
+++ b/packages/hooks/src/useLocalStorageState/demo/demo3.tsx
@@ -1,37 +1,34 @@
 /**
- * title: Persist objects with function updater
- * desc: function updater is also acceptable with useLocalStorageState.
+ * title: Custom serialization and deserialization functions
+ * desc: You may not need the default `JSON.stringify/JSON.parse` to serialize string.
  *
- * title.zh-CN: 使用 function updater 存储
- * desc.zh-CN: useLocalStorageState 里也可以用 function updater，就像 useState 那样。
+ * title.zh-CN: 自定义序列化和反序列化函数
+ * desc.zh-CN: 对于普通的字符串，可能你不需要默认的 `JSON.stringify/JSON.parse` 来序列化。
  */
 
 import React from 'react';
 import { useLocalStorageState } from 'ahooks';
 
-interface IUser {
-  id: number;
-  name: string;
-  age: number;
-}
-
 export default function () {
-  const [user, setUser] = useLocalStorageState('user', {
-    id: 9234634791,
-    name: 'Zhangsan',
-    age: 33,
-  } as IUser);
+  const [message, setMessage] = useLocalStorageState('use-local-storage-state-demo3', {
+    defaultValue: 'Hello~',
+    serializer: (v) => v,
+    deserializer: (v) => v,
+  });
 
   return (
     <>
       <input
-        style={{ width: 200 }}
-        defaultValue={user.name}
-        placeholder="input user name"
-        onChange={(e) => {
-          setUser((u: IUser) => ({ ...u, name: e.target.value }));
-        }}
+        value={message || ''}
+        placeholder="Please enter some words..."
+        onChange={(e) => setMessage(e.target.value)}
       />
+      <button style={{ margin: '0 8px' }} type="button" onClick={() => setMessage('Hello~')}>
+        Reset
+      </button>
+      <button type="button" onClick={() => setMessage()}>
+        Clear
+      </button>
     </>
   );
 }
diff --git a/packages/hooks/src/useLocalStorageState/index.en-US.md b/packages/hooks/src/useLocalStorageState/index.en-US.md
index 9e3993bb..1eb13cd0 100644
--- a/packages/hooks/src/useLocalStorageState/index.en-US.md
+++ b/packages/hooks/src/useLocalStorageState/index.en-US.md
@@ -10,31 +10,45 @@ group:
 
 # useLocalStorageState
 
-A Hook for persisting state into localStorage.
+<Tag lang="zh-CN" tags="ssr"></Tag>
+
+A Hook can store state into localStorage.
 
 ## Examples
 
-### Persist state into localStorage
+### Store state into localStorage
 
 <code src="./demo/demo1.tsx" />
 
-### Persist objects
+### Store complex types of data
 
 <code src="./demo/demo2.tsx" />
 
-### Persist objects with function updater
+### Custom serialization and deserialization functions
 
 <code src="./demo/demo3.tsx" />
 
 ## API
 
+If you want to delete this record from localStorage, you can use `setState()` or `setState(undefined)`.
+
 ```typescript
+interface Options<T> {
+  defaultValue?: T | (() => T);
+  serializer?: (value: T) => string;
+  deserializer?: (value: string) => T;
+}
+
 const [state, setState] = useLocalStorageState<T>(
   key: string,
-  defaultValue?: T | (() => T),
+  options: Options<T>
 ): [T?, (value?: T | ((previousState: T) => T)) => void]
 ```
 
-The API is very similiar to `useState`, but there is an extra `key` parameter for specifying the key of localStorage. The return value is the same as `useState`. When `setState` gets called, it will update the new value to localStorage automatically.
+### Options
 
-If you want to delete this record from localStorage, you can use `setState()` or `setState(undefined)`.
+| Property     | Description                   | Type                     | Default          |
+|--------------|-------------------------------|--------------------------|------------------|
+| defaultValue | Default value                 | `any \| (() => any)`     | -                |
+| serializer   | Custom serialization method   | `(value: any) => string` | `JSON.stringify` |
+| deserializer | Custom deserialization method | `(value: string) => any` | `JSON.parse`     |
\ No newline at end of file
diff --git a/packages/hooks/src/useLocalStorageState/index.ts b/packages/hooks/src/useLocalStorageState/index.ts
index ad57392c..7b8ca27c 100644
--- a/packages/hooks/src/useLocalStorageState/index.ts
+++ b/packages/hooks/src/useLocalStorageState/index.ts
@@ -1,7 +1,6 @@
 import { createUseStorageState } from '../createUseStorageState';
+import { isBrowser } from '../utils/dom2';
 
-const useLocalStorageState = createUseStorageState(
-  typeof window === 'object' ? window.localStorage : null,
-);
+const useLocalStorageState = createUseStorageState(() => (isBrowser ? localStorage : undefined));
 
 export default useLocalStorageState;
diff --git a/packages/hooks/src/useLocalStorageState/index.zh-CN.md b/packages/hooks/src/useLocalStorageState/index.zh-CN.md
index 61b27839..2cbef99c 100644
--- a/packages/hooks/src/useLocalStorageState/index.zh-CN.md
+++ b/packages/hooks/src/useLocalStorageState/index.zh-CN.md
@@ -10,31 +10,45 @@ group:
 
 # useLocalStorageState
 
-一个可以将状态持久化存储在 localStorage 中的 Hook 。
+<Tag lang="zh-CN" tags="ssr&crossPlatform"></Tag>
+
+一个可以将状态存储在 localStorage 中的 Hook 。
 
 ## 代码演示
 
-### 将 state 持久化在 localStorage 中
+### 将 state 存储在 localStorage 中
 
 <code src="./demo/demo1.tsx" />
 
-### 存储对象
+### 存储复杂类型数据
 
 <code src="./demo/demo2.tsx" />
 
-### 使用 function updater 存储
+### 自定义序列化和反序列化函数
 
 <code src="./demo/demo3.tsx" />
 
 ## API
 
+如果想从 localStorage 中删除这条数据，可以使用 `setState()` 或 `setState(undefined)` 。
+
 ```typescript
+interface Options<T> {
+  defaultValue?: T | (() => T);
+  serializer?: (value: T) => string;
+  deserializer?: (value: string) => T;
+}
+
 const [state, setState] = useLocalStorageState<T>(
   key: string,
-  defaultValue?: T | (() => T),
+  options: Options<T>
 ): [T?, (value?: T | ((previousState: T) => T)) => void]
 ```
 
-它的API和 `useState` 非常类似，但是多了一个参数 `key` ，用来指定在 localStorage 中存储时所使用的 `key` 。而它的返回值类型和 `useState` 保持了一致，当调用 `setState` 时，它会自动将新值写入到 localStorage 中。
+### Options
 
-如果想从 localStorage 中删除这条数据，可以使用 `setState()` 或 `setState(undefined)` 。
+| 参数         | 说明               | 类型                     | 默认值           |
+|--------------|--------------------|--------------------------|------------------|
+| defaultValue | 默认值             | `any \| (() => any)`     | -                |
+| serializer   | 自定义序列化方法   | `(value: any) => string` | `JSON.stringify` |
+| deserializer | 自定义反序列化方法 | `(value: string) => any` | `JSON.parse`     |
diff --git a/packages/hooks/src/useSessionStorageState/demo/demo1.tsx b/packages/hooks/src/useSessionStorageState/demo/demo1.tsx
deleted file mode 100644
index fda4faec..00000000
--- a/packages/hooks/src/useSessionStorageState/demo/demo1.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * title: Persist state into sessionStorage
- * desc: Refresh this page and you will find the value of input box get restored from sessionStorage.
- *
- * title.zh-CN: 将 state 持久化在 sessionStorage 中
- * desc.zh-CN: 刷新页面后，可以看到输入框中的内容被从 sessionStorage 中恢复了。
- */
-
-import { useSessionStorageState } from 'ahooks';
-import React from 'react';
-
-export default function () {
-  const [message, setMessage] = useSessionStorageState('user-message', 'Hello~');
-  return (
-    <>
-      <input
-        value={message}
-        onChange={(e) => {
-          setMessage(e.target.value);
-        }}
-        placeholder="Please enter some words..."
-        style={{ width: 200, marginRight: 8 }}
-      />
-      <button
-        type="button"
-        onClick={() => {
-          setMessage();
-        }}
-      >
-        Reset
-      </button>
-    </>
-  );
-}
diff --git a/packages/hooks/src/useSessionStorageState/demo/demo2.tsx b/packages/hooks/src/useSessionStorageState/demo/demo2.tsx
deleted file mode 100644
index 67a4f991..00000000
--- a/packages/hooks/src/useSessionStorageState/demo/demo2.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * title: Persist objects
- * desc: useSessionStorageState will do the serialization and deserialization work automatically.
- *
- * title.zh-CN: 存储对象
- * desc.zh-CN: useSessionStorageState 会自动处理序列化和反序列化的操作。
- */
-
-import { useSessionStorageState } from 'ahooks';
-import React from 'react';
-
-const defaultArray = ['a', 'e', 'i', 'o', 'u'];
-
-export default function () {
-  const [value, setValue] = useSessionStorageState('cascader', defaultArray);
-
-  return (
-    <>
-      <p>{value.join('-')}</p>
-      <button
-        type="button"
-        style={{ marginRight: 8 }}
-        onClick={() => setValue([...value, Math.random().toString(36).slice(-1)])}
-      >
-        push random
-      </button>
-      <button type="button" onClick={() => setValue(defaultArray)}>
-        reset
-      </button>
-    </>
-  );
-}
diff --git a/packages/hooks/src/useSessionStorageState/demo/demo3.tsx b/packages/hooks/src/useSessionStorageState/demo/demo3.tsx
deleted file mode 100644
index 8ef6aa4e..00000000
--- a/packages/hooks/src/useSessionStorageState/demo/demo3.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * title: Persist objects with function updater
- * desc: function updater is also acceptable with useSessionStorageState.
- *
- * title.zh-CN: 使用 function updater 存储
- * desc.zh-CN: useSessionStorageState 里也可以用 function updater，就像 useState 那样。
- */
-
-import React from 'react';
-import { useSessionStorageState } from 'ahooks';
-
-interface IUser {
-  id: number;
-  name: string;
-  age: number;
-}
-
-export default function () {
-  const [user, setUser] = useSessionStorageState('user', {
-    id: 9234634791,
-    name: 'Zhangsan',
-    age: 33,
-  } as IUser);
-
-  return (
-    <>
-      <input
-        style={{ width: 200 }}
-        defaultValue={user.name}
-        placeholder="input user name"
-        onChange={(e) => {
-          setUser((u: IUser) => ({ ...u, name: e.target.value }));
-        }}
-      />
-    </>
-  );
-}
diff --git a/packages/hooks/src/useSessionStorageState/index.en-US.md b/packages/hooks/src/useSessionStorageState/index.en-US.md
index 9635aa6c..a13c8d98 100644
--- a/packages/hooks/src/useSessionStorageState/index.en-US.md
+++ b/packages/hooks/src/useSessionStorageState/index.en-US.md
@@ -10,31 +10,6 @@ group:
 
 # useSessionStorageState
 
-A Hook for persisting state into sessionStorage.
+A Hook for Store state into sessionStorage.
 
-## Examples
-
-### Persist state into sessionStorage
-
-<code src="./demo/demo1.tsx" />
-
-### Persist objects
-
-<code src="./demo/demo2.tsx" />
-
-### Persist objects with function updater
-
-<code src="./demo/demo3.tsx" />
-
-## API
-
-```typescript
-const [state, setState] = useSessionStorageState<T>(
-  key: string,
-  defaultValue?: T | (() => T),
-): [T?, (value?: T | ((previousState: T) => T)) => void]
-```
-
-The API is very similiar to `useState`, but there is an extra `key` parameter for specifying the key of sessionStorage. The return value is the same as `useState`. When `setState` gets called, it will update the new value to sessionStorage automatically.
-
-If you want to delete this record from sessionStorage, you can use `setState()` or `setState(undefined)`.
+Usage is exactly the same as [useLocalStorageState](./use-local-storage-state).
diff --git a/packages/hooks/src/useSessionStorageState/index.ts b/packages/hooks/src/useSessionStorageState/index.ts
index 57b97f5b..ece238d6 100644
--- a/packages/hooks/src/useSessionStorageState/index.ts
+++ b/packages/hooks/src/useSessionStorageState/index.ts
@@ -1,7 +1,8 @@
 import { createUseStorageState } from '../createUseStorageState';
+import { isBrowser } from '../utils/dom2';
 
-const useSessionStorageState = createUseStorageState(
-  typeof window === 'object' ? window.sessionStorage : null,
+const useSessionStorageState = createUseStorageState(() =>
+  isBrowser ? sessionStorage : undefined,
 );
 
 export default useSessionStorageState;
diff --git a/packages/hooks/src/useSessionStorageState/index.zh-CN.md b/packages/hooks/src/useSessionStorageState/index.zh-CN.md
index 1246aca4..95ac0a8a 100644
--- a/packages/hooks/src/useSessionStorageState/index.zh-CN.md
+++ b/packages/hooks/src/useSessionStorageState/index.zh-CN.md
@@ -10,31 +10,6 @@ group:
 
 # useSessionStorageState
 
-一个可以将状态持久化存储在 sessionStorage 中的 Hook。
+一个可以将状态存储在 sessionStorage 中的 Hook。
 
-## 代码演示
-
-### 将 state 持久化在 sessionStorage 中
-
-<code src="./demo/demo1.tsx" />
-
-### 存储对象
-
-<code src="./demo/demo2.tsx" />
-
-### 使用 function updater 存储
-
-<code src="./demo/demo3.tsx" />
-
-## API
-
-```typescript
-const [state, setState] = useSessionStorageState<T>(
-  key: string,
-  defaultValue?: T | (() => T),
-): [T?, (value?: T | ((previousState: T) => T)) => void]
-```
-
-它的API和 `useState` 非常类似，但是多了一个参数 `key` ，用来指定在 sessionStorage 中存储时所使用的 `key` 。而它的返回值类型和 `useState` 保持了一致，当调用 `setState` 时，它会自动将新值写入到 sessionStorage 中。
-
-如果想从 sessionStorage 中删除这条数据，可以使用 `setState()` 或 `setState(undefined)` 。
+用法与 [useLocalStorageState](./use-local-storage-state) 一致。
\ No newline at end of file


Test patch:
diff --git a/packages/hooks/src/createUseStorageState/__tests__/index.test.ts b/packages/hooks/src/createUseStorageState/__tests__/index.test.ts
index b49d117c..5cf28558 100644
--- a/packages/hooks/src/createUseStorageState/__tests__/index.test.ts
+++ b/packages/hooks/src/createUseStorageState/__tests__/index.test.ts
@@ -48,11 +48,11 @@ interface StorageStateProps<T> {
 describe('useStorageState', () => {
   const setUp = <T>(props: StorageStateProps<T>) => {
     const storage = new TestStorage();
-    const useStorageState = createUseStorageState(storage);
+    const useStorageState = createUseStorageState(() => storage);
 
     return renderHook(
       ({ key, defaultValue }: StorageStateProps<T>) => {
-        const [state, setState] = useStorageState(key, defaultValue);
+        const [state, setState] = useStorageState(key, { defaultValue });
 
         return { state, setState };
       },
diff --git a/packages/hooks/src/useLocalStorageState/__tests__/index.test.ts b/packages/hooks/src/useLocalStorageState/__tests__/index.test.ts
index 78786b1a..72ede2c7 100644
--- a/packages/hooks/src/useLocalStorageState/__tests__/index.test.ts
+++ b/packages/hooks/src/useLocalStorageState/__tests__/index.test.ts
@@ -8,7 +8,7 @@ describe('useLocalStorageState', () => {
 
   const setUp = <T>(key: string, value: T) =>
     renderHook(() => {
-      const [state, setState] = useLocalStorageState<T>(key, value);
+      const [state, setState] = useLocalStorageState<T>(key, { defaultValue: value });
       return {
         state,
         setState,
diff --git a/packages/hooks/src/useSessionStorageState/__tests__/index.test.ts b/packages/hooks/src/useSessionStorageState/__tests__/index.test.ts
index a2f1cf31..d4dc6550 100644
--- a/packages/hooks/src/useSessionStorageState/__tests__/index.test.ts
+++ b/packages/hooks/src/useSessionStorageState/__tests__/index.test.ts
@@ -8,7 +8,7 @@ describe('useSessionStorageState', () => {
 
   const setUp = <T>(key: string, value: T) =>
     renderHook(() => {
-      const [state, setState] = useSessionStorageState<T>(key, value);
+      const [state, setState] = useSessionStorageState<T>(key, { defaultValue: value });
       return {
         state,
         setState,