Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: useStorage expirationTime #2672

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions packages/hooks/src/createUseStorageState/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type SetState<S> = S | ((prevState?: S) => S);
export interface Options<T> {
defaultValue?: T | (() => T);
listenStorageChange?: boolean;
expirationTime?: number;
serializer?: (value: T) => string;
deserializer?: (value: string) => T;
onError?: (error: unknown) => void;
Expand All @@ -21,6 +22,7 @@ export function createUseStorageState(getStorage: () => Storage | undefined) {
let storage: Storage | undefined;
const {
listenStorageChange = false,
expirationTime,
onError = (e) => {
console.error(e);
},
Expand All @@ -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() {
Expand Down
13 changes: 13 additions & 0 deletions packages/hooks/src/useLocalStorageState/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
32 changes: 32 additions & 0 deletions packages/hooks/src/useLocalStorageState/demo/demo5.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<p>{message}</p>
<button
style={{ margin: '0 8px' }}
onClick={() => {
setMessage('Hello~' + new Date().toLocaleTimeString());
}}
>
Update Message
</button>
<button onClick={() => window.location.reload()}>Refresh</button>
</div>
);
}
7 changes: 7 additions & 0 deletions packages/hooks/src/useLocalStorageState/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ A Hook that store state into localStorage.

<code src="./demo/demo4.tsx" />

### Custom expiration time

<code src="./demo/demo5.tsx" />

## API

If you want to delete this record from localStorage, you can use `setState()` or `setState(undefined)`.
Expand All @@ -34,6 +38,8 @@ type SetState<S> = S | ((prevState?: S) => S);

interface Options<T> {
defaultValue?: T | (() => T);
listenStorageChange?: boolean;
expirationTime?: number;
serializer?: (value: T) => string;
deserializer?: (value: string) => T;
onError?: (error: unknown) => void;
Expand All @@ -58,6 +64,7 @@ const [state, setState] = useLocalStorageState<T>(
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ----------------------------- |
| 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) }` |
Expand Down
7 changes: 7 additions & 0 deletions packages/hooks/src/useLocalStorageState/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ nav:

<code src="./demo/demo4.tsx" />

### 自定义过期时间

<code src="./demo/demo5.tsx" />

## API

如果想从 localStorage 中删除这条数据,可以使用 `setState()` 或 `setState(undefined)` 。
Expand All @@ -34,6 +38,8 @@ type SetState<S> = S | ((prevState?: S) => S);

interface Options<T> {
defaultValue?: T | (() => T);
listenStorageChange?: boolean;
expirationTime?: number;
serializer?: (value: T) => string;
deserializer?: (value: string) => T;
onError?: (error: unknown) => void;
Expand All @@ -58,6 +64,7 @@ const [state, setState] = useLocalStorageState<T>(
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ----------------------------- |
| 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) }` |
Expand Down