From 7891c7c54a8c38c87488059c388c23817b136e49 Mon Sep 17 00:00:00 2001 From: cwen Date: Mon, 11 Nov 2024 14:57:55 +0800 Subject: [PATCH] feat: useStorage expirationTime --- .../hooks/src/createUseStorageState/index.ts | 38 +++++++++++++++---- .../__tests__/index.test.ts | 13 +++++++ .../src/useLocalStorageState/demo/demo5.tsx | 32 ++++++++++++++++ .../src/useLocalStorageState/index.en-US.md | 7 ++++ .../src/useLocalStorageState/index.zh-CN.md | 7 ++++ 5 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 packages/hooks/src/useLocalStorageState/demo/demo5.tsx diff --git a/packages/hooks/src/createUseStorageState/index.ts b/packages/hooks/src/createUseStorageState/index.ts index 70a3e2cd9e..8e9754ba55 100644 --- a/packages/hooks/src/createUseStorageState/index.ts +++ b/packages/hooks/src/createUseStorageState/index.ts @@ -11,6 +11,7 @@ export type SetState = S | ((prevState?: S) => S); export interface Options { defaultValue?: T | (() => T); listenStorageChange?: boolean; + expirationTime?: number; serializer?: (value: T) => string; deserializer?: (value: string) => T; onError?: (error: unknown) => void; @@ -21,6 +22,7 @@ export function createUseStorageState(getStorage: () => Storage | undefined) { let storage: Storage | undefined; const { listenStorageChange = false, + expirationTime, onError = (e) => { console.error(e); }, @@ -33,18 +35,38 @@ export function createUseStorageState(getStorage: () => Storage | undefined) { onError(err); } - const serializer = (value: T) => { - if (options.serializer) { - return options.serializer(value); + const serializer = (value: T): string => { + if (expirationTime) { + const storageData = { + data: options.serializer ? options.serializer(value) : value, + timestamp: Date.now(), + }; + return JSON.stringify(storageData); } - return JSON.stringify(value); + return options.serializer ? options.serializer(value) : JSON.stringify(value); }; - const deserializer = (value: string): T => { - if (options.deserializer) { - return options.deserializer(value); + const deserializer = (value: string): T | undefined => { + try { + const parsed = JSON.parse(value); + if (expirationTime) { + if (parsed.timestamp && Date.now() - parsed.timestamp > expirationTime) { + storage?.removeItem(key); + return undefined; + } + if (options.deserializer) { + return options.deserializer(parsed.data as string); + } + return parsed.data; + } + if (options.deserializer) { + return options.deserializer(value); + } + return parsed; + } catch (e) { + onError(e); + return undefined; } - return JSON.parse(value); }; function getStoredValue() { diff --git a/packages/hooks/src/useLocalStorageState/__tests__/index.test.ts b/packages/hooks/src/useLocalStorageState/__tests__/index.test.ts index 1368ebe922..7ace75dba4 100644 --- a/packages/hooks/src/useLocalStorageState/__tests__/index.test.ts +++ b/packages/hooks/src/useLocalStorageState/__tests__/index.test.ts @@ -127,4 +127,17 @@ describe('useLocalStorageState', () => { expect(hook.result.current.state).toBe('qux'); expect(anotherHook.result.current.state).toBe('qux'); }); + + it('should support expiration time', async () => { + const LOCAL_STORAGE_KEY = 'test-expiration-time'; + const hook = setUp(LOCAL_STORAGE_KEY, 'initial', { expirationTime: 1000 }); + expect(hook.result.current.state).toBe('initial'); + act(() => { + hook.result.current.setState('updated'); + }); + expect(hook.result.current.state).toBe('updated'); + await new Promise((resolve) => setTimeout(resolve, 1500)); + const newHook = setUp(LOCAL_STORAGE_KEY, undefined, { expirationTime: 1000 }); + expect(newHook.result.current.state).toBeUndefined(); + }); }); diff --git a/packages/hooks/src/useLocalStorageState/demo/demo5.tsx b/packages/hooks/src/useLocalStorageState/demo/demo5.tsx new file mode 100644 index 0000000000..4814e35e23 --- /dev/null +++ b/packages/hooks/src/useLocalStorageState/demo/demo5.tsx @@ -0,0 +1,32 @@ +/** + * title: localStorage expiration time + * desc: Data will be cleared after expiration time + * + * title.zh-CN: localStorage 过期时间 + * desc.zh-CN: 数据将在过期时间后自动清除 + */ + +import React from 'react'; +import { useLocalStorageState } from 'ahooks'; + +export default function () { + const [message, setMessage] = useLocalStorageState('use-local-storage-state-demo5', { + defaultValue: 'Hello~', + expirationTime: 5000, + }); + + return ( +
+

{message}

+ + +
+ ); +} diff --git a/packages/hooks/src/useLocalStorageState/index.en-US.md b/packages/hooks/src/useLocalStorageState/index.en-US.md index f0001eaacf..444de9b4d6 100644 --- a/packages/hooks/src/useLocalStorageState/index.en-US.md +++ b/packages/hooks/src/useLocalStorageState/index.en-US.md @@ -25,6 +25,10 @@ A Hook that store state into localStorage. +### Custom expiration time + + + ## API If you want to delete this record from localStorage, you can use `setState()` or `setState(undefined)`. @@ -34,6 +38,8 @@ type SetState = S | ((prevState?: S) => S); interface Options { defaultValue?: T | (() => T); + listenStorageChange?: boolean; + expirationTime?: number; serializer?: (value: T) => string; deserializer?: (value: string) => T; onError?: (error: unknown) => void; @@ -58,6 +64,7 @@ const [state, setState] = useLocalStorageState( | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ----------------------------- | | defaultValue | Default value | `any \| (() => any)` | - | | listenStorageChange | Whether to listen storage changes. If `true`, when the stored value changes, all `useLocalStorageState` with the same `key` will synchronize their states, including different tabs of the same browser | `boolean` | `false` | +| expirationTime | Expiration time, in milliseconds. If the expiration time is reached, the data will be cleared. | `number` | - | | serializer | Custom serialization method | `(value: any) => string` | `JSON.stringify` | | deserializer | Custom deserialization method | `(value: string) => any` | `JSON.parse` | | onError | On error callback | `(error: unknown) => void` | `(e) => { console.error(e) }` | diff --git a/packages/hooks/src/useLocalStorageState/index.zh-CN.md b/packages/hooks/src/useLocalStorageState/index.zh-CN.md index a9b84ba3f0..daa9a903d7 100644 --- a/packages/hooks/src/useLocalStorageState/index.zh-CN.md +++ b/packages/hooks/src/useLocalStorageState/index.zh-CN.md @@ -25,6 +25,10 @@ nav: +### 自定义过期时间 + + + ## API 如果想从 localStorage 中删除这条数据,可以使用 `setState()` 或 `setState(undefined)` 。 @@ -34,6 +38,8 @@ type SetState = S | ((prevState?: S) => S); interface Options { defaultValue?: T | (() => T); + listenStorageChange?: boolean; + expirationTime?: number; serializer?: (value: T) => string; deserializer?: (value: string) => T; onError?: (error: unknown) => void; @@ -58,6 +64,7 @@ const [state, setState] = useLocalStorageState( | ------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ----------------------------- | | defaultValue | 默认值 | `any \| (() => any)` | - | | listenStorageChange | 是否监听存储变化。如果是 `true`,当存储值变化时,所有 `key` 相同的 `useLocalStorageState` 会同步状态,包括同一浏览器不同 tab 之间 | `boolean` | `false` | +| expirationTime | 过期时间,单位毫秒。如果过期时间到达,数据将被清除。 | `number` | - | | serializer | 自定义序列化方法 | `(value: any) => string` | `JSON.stringify` | | deserializer | 自定义反序列化方法 | `(value: string) => any` | `JSON.parse` | | onError | 错误回调函数 | `(error: unknown) => void` | `(e) => { console.error(e) }` |