Skip to content

Commit 8b5f382

Browse files
committed
feat: add useIndexDBState hook
1 parent c7bb04c commit 8b5f382

File tree

7 files changed

+598
-0
lines changed

7 files changed

+598
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# useIndexDBState
2+
3+
A React Hook that stores state into IndexedDB.
4+
5+
## Examples
6+
7+
### Basic usage
8+
9+
```jsx
10+
import React from 'react';
11+
import { useIndexDBState } from 'ahooks';
12+
13+
export default function Demo() {
14+
const [message, setMessage] = useIndexDBState('message', {
15+
defaultValue: 'Hello',
16+
});
17+
18+
return (
19+
<>
20+
<input
21+
value={message || ''}
22+
onChange={(e) => setMessage(e.target.value)}
23+
/>
24+
<button onClick={() => setMessage(undefined)}>Reset</button>
25+
</>
26+
);
27+
}
28+
```
29+
30+
### Store complex object
31+
32+
```jsx
33+
import React from 'react';
34+
import { useIndexDBState } from 'ahooks';
35+
36+
export default function Demo() {
37+
const [user, setUser] = useIndexDBState('user', {
38+
defaultValue: { name: 'Ahooks', age: 1 },
39+
});
40+
41+
return (
42+
<>
43+
<pre>{JSON.stringify(user, null, 2)}</pre>
44+
<button
45+
onClick={() => setUser({ name: 'New Name', age: 2 })}
46+
>
47+
Update User
48+
</button>
49+
</>
50+
);
51+
}
52+
```
53+
54+
## API
55+
56+
```typescript
57+
type SetState<S> = S | ((prevState?: S) => S);
58+
59+
interface Options<T> {
60+
defaultValue?: T | (() => T);
61+
dbName?: string;
62+
storeName?: string;
63+
version?: number;
64+
onError?: (error: unknown) => void;
65+
}
66+
67+
const [state, setState] = useIndexDBState<T>(key: string, options?: Options<T>);
68+
```
69+
70+
### Params
71+
72+
| Property | Description | Type | Default |
73+
|----------|-------------|------|---------|
74+
| key | The key of the IndexedDB record | `string` | - |
75+
| options | Optional configuration | `Options` | - |
76+
77+
### Options
78+
79+
| Property | Description | Type | Default |
80+
|----------|-------------|------|---------|
81+
| defaultValue | Default value | `T \| (() => T)` | `undefined` |
82+
| dbName | Name of the IndexedDB database | `string` | `ahooks-indexdb` |
83+
| storeName | Name of the object store | `string` | `ahooks-store` |
84+
| version | Version of the database | `number` | `1` |
85+
| onError | Error handler | `(error: unknown) => void` | `(e) => console.error(e)` |
86+
87+
### Result
88+
89+
| Property | Description | Type |
90+
|----------|-------------|------|
91+
| state | Current state | `T` |
92+
| setState | Set state | `(value?: SetState<T>) => void` |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { renderHook, act } from '@testing-library/react';
2+
import useIndexDBState from '../index';
3+
4+
// 模拟 IndexedDB
5+
const mockIndexedDB = {
6+
open: jest.fn(),
7+
};
8+
9+
const mockIDBOpenDBRequest = {
10+
onerror: null,
11+
onsuccess: null,
12+
onupgradeneeded: null,
13+
result: {
14+
transaction: jest.fn(),
15+
close: jest.fn(),
16+
objectStoreNames: {
17+
contains: jest.fn().mockReturnValue(false),
18+
},
19+
createObjectStore: jest.fn(),
20+
},
21+
};
22+
23+
const mockObjectStore = {
24+
get: jest.fn(),
25+
put: jest.fn(),
26+
delete: jest.fn(),
27+
};
28+
29+
const mockTransaction = {
30+
objectStore: jest.fn().mockReturnValue(mockObjectStore),
31+
};
32+
33+
// 模拟 window.indexedDB
34+
Object.defineProperty(window, 'indexedDB', {
35+
value: mockIndexedDB,
36+
writable: true,
37+
});
38+
39+
describe('useIndexDBState', () => {
40+
beforeEach(() => {
41+
// 设置全局 indexedDB 模拟
42+
mockIndexedDB.open.mockReturnValue(mockIDBOpenDBRequest);
43+
mockIDBOpenDBRequest.result.transaction.mockReturnValue(mockTransaction);
44+
45+
// 清除之前的模拟调用
46+
jest.clearAllMocks();
47+
});
48+
49+
it('should initialize with default value', async () => {
50+
const { result } = renderHook(() =>
51+
useIndexDBState('test-key', {
52+
defaultValue: 'default value',
53+
}),
54+
);
55+
56+
expect(result.current[0]).toBe('default value');
57+
});
58+
59+
it('should update state when setState is called', async () => {
60+
const { result } = renderHook(() =>
61+
useIndexDBState('test-key', {
62+
defaultValue: 'default value',
63+
}),
64+
);
65+
66+
act(() => {
67+
result.current[1]('new value');
68+
});
69+
70+
expect(result.current[0]).toBe('new value');
71+
});
72+
73+
it('should handle function updater', async () => {
74+
const { result } = renderHook(() =>
75+
useIndexDBState('test-key', {
76+
defaultValue: 'default value',
77+
}),
78+
);
79+
80+
act(() => {
81+
result.current[1]((prev) => `${prev} updated`);
82+
});
83+
84+
expect(result.current[0]).toBe('default value updated');
85+
});
86+
87+
it('should handle undefined value', async () => {
88+
const { result } = renderHook(() =>
89+
useIndexDBState('test-key', {
90+
defaultValue: 'default value',
91+
}),
92+
);
93+
94+
act(() => {
95+
result.current[1](undefined);
96+
});
97+
98+
expect(result.current[0]).toBeUndefined();
99+
});
100+
101+
it('should call onError when an error occurs', async () => {
102+
const onError = jest.fn();
103+
mockIndexedDB.open.mockImplementationOnce(() => {
104+
throw new Error('Test error');
105+
});
106+
107+
renderHook(() =>
108+
useIndexDBState('test-key', {
109+
defaultValue: 'default value',
110+
onError,
111+
}),
112+
);
113+
114+
expect(onError).toHaveBeenCalled();
115+
});
116+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* title: Basic usage
3+
* desc: Persist state into IndexedDB
4+
*
5+
* title.zh-CN: 基础用法
6+
* desc.zh-CN: 将状态持久化存储到 IndexedDB 中
7+
*/
8+
9+
import React from 'react';
10+
import useIndexDBState from '../index';
11+
12+
export default function Demo() {
13+
const [message, setMessage] = useIndexDBState<string>('message', {
14+
defaultValue: 'Hello',
15+
});
16+
17+
return (
18+
<>
19+
<input
20+
value={message || ''}
21+
placeholder="Please enter some text"
22+
onChange={(e) => setMessage(e.target.value)}
23+
style={{ width: 200, marginRight: 16 }}
24+
/>
25+
<button
26+
type="button"
27+
onClick={() => setMessage(undefined)}
28+
style={{ marginRight: 16 }}
29+
>
30+
Reset
31+
</button>
32+
<button
33+
type="button"
34+
onClick={() => window.location.reload()}
35+
>
36+
Refresh
37+
</button>
38+
</>
39+
);
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* title: Store complex object
3+
* desc: useIndexDBState can store complex objects
4+
*
5+
* title.zh-CN: 存储复杂对象
6+
* desc.zh-CN: useIndexDBState 可以存储复杂对象
7+
*/
8+
9+
import React, { useState } from 'react';
10+
import useIndexDBState from '../index';
11+
12+
interface User {
13+
name: string;
14+
age: number;
15+
}
16+
17+
export default function Demo() {
18+
const [user, setUser] = useIndexDBState<User>('user', {
19+
defaultValue: { name: 'Ahooks', age: 1 },
20+
});
21+
22+
const [inputValue, setInputValue] = useState('');
23+
const [inputAge, setInputAge] = useState('');
24+
25+
return (
26+
<>
27+
<div>
28+
<label>Name: </label>
29+
<input
30+
value={inputValue}
31+
placeholder="Please enter name"
32+
onChange={(e) => setInputValue(e.target.value)}
33+
style={{ width: 200, marginRight: 16 }}
34+
/>
35+
</div>
36+
<div style={{ marginTop: 8 }}>
37+
<label>Age: </label>
38+
<input
39+
value={inputAge}
40+
placeholder="Please enter age"
41+
onChange={(e) => setInputAge(e.target.value)}
42+
style={{ width: 200, marginRight: 16 }}
43+
/>
44+
</div>
45+
<div style={{ marginTop: 16 }}>
46+
<button
47+
type="button"
48+
onClick={() => {
49+
setUser({ name: inputValue, age: Number(inputAge) });
50+
setInputValue('');
51+
setInputAge('');
52+
}}
53+
style={{ marginRight: 16 }}
54+
>
55+
Save
56+
</button>
57+
<button
58+
type="button"
59+
onClick={() => setUser(undefined)}
60+
style={{ marginRight: 16 }}
61+
>
62+
Reset
63+
</button>
64+
<button
65+
type="button"
66+
onClick={() => window.location.reload()}
67+
>
68+
Refresh
69+
</button>
70+
</div>
71+
<div style={{ marginTop: 16 }}>
72+
<pre>{JSON.stringify(user, null, 2)}</pre>
73+
</div>
74+
</>
75+
);
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
nav:
3+
path: /hooks
4+
---
5+
6+
# useIndexDBState
7+
8+
A Hook that stores state into IndexedDB.
9+
10+
## Examples
11+
12+
### Basic usage
13+
14+
<code src="./demo/demo1.tsx" />
15+
16+
### Complex object
17+
18+
<code src="./demo/demo2.tsx" />
19+
20+
## API
21+
22+
```typescript
23+
type SetState<S> = S | ((prevState?: S) => S);
24+
25+
interface Options<T> {
26+
defaultValue?: T | (() => T);
27+
dbName?: string;
28+
storeName?: string;
29+
version?: number;
30+
onError?: (error: unknown) => void;
31+
}
32+
33+
const [state, setState] = useIndexDBState<T>(key: string, options?: Options<T>);
34+
```
35+
36+
### Params
37+
38+
| Property | Description | Type | Default |
39+
|----------|-------------|------|---------|
40+
| key | The key of the IndexedDB record | `string` | - |
41+
| options | Optional configuration | `Options` | - |
42+
43+
### Options
44+
45+
| Property | Description | Type | Default |
46+
|----------|-------------|------|---------|
47+
| defaultValue | Default value | `T \| (() => T)` | `undefined` |
48+
| dbName | Name of the IndexedDB database | `string` | `ahooks-indexdb` |
49+
| storeName | Name of the object store | `string` | `ahooks-store` |
50+
| version | Version of the database | `number` | `1` |
51+
| onError | Error handler | `(error: unknown) => void` | `(e) => console.error(e)` |
52+
53+
### Result
54+
55+
| Property | Description | Type |
56+
|----------|-------------|------|
57+
| state | Current state | `T` |
58+
| setState | Set state | `(value?: SetState<T>) => void` |

0 commit comments

Comments
 (0)