@akb2/react-use-local-storage 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -18,54 +18,69 @@ The package provides ESM, CommonJS, and TypeScript declarations.
|
|
|
18
18
|
import { useLocalStorageState } from "react-use-local-storage";
|
|
19
19
|
|
|
20
20
|
export const Counter = () => {
|
|
21
|
-
const [count, setCount] = useLocalStorageState<number>("counter");
|
|
21
|
+
const [count, setCount] = useLocalStorageState<number>("counter", 0);
|
|
22
22
|
|
|
23
23
|
return (
|
|
24
|
-
<button onClick={() => setCount(
|
|
25
|
-
Clicks: {count
|
|
24
|
+
<button onClick={() => setCount(count + 1)}>
|
|
25
|
+
Clicks: {count}
|
|
26
26
|
</button>
|
|
27
27
|
);
|
|
28
28
|
};
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
The
|
|
31
|
+
The optional second argument is a fallback for a missing key. Without a fallback, a missing key returns `null`. The fallback is used for reading and rendering only; it is not written to storage. Removing the key makes the hook return its fallback again.
|
|
32
32
|
|
|
33
33
|
## API
|
|
34
34
|
|
|
35
|
-
### `useLocalStorageState<T>(key)`
|
|
35
|
+
### `useLocalStorageState<T>(key, fallback?)`
|
|
36
36
|
|
|
37
37
|
```ts
|
|
38
|
-
const [value, setValue, storageKey] = useLocalStorageState<string>("name");
|
|
38
|
+
const [value, setValue, storageKey] = useLocalStorageState<string>("name", "Guest");
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
| Element | Description |
|
|
42
42
|
| --- | --- |
|
|
43
|
-
| `value` | The
|
|
44
|
-
| `setValue` | Writes a
|
|
43
|
+
| `value` | The stored value, or the fallback for a missing key (`null` by default) |
|
|
44
|
+
| `setValue` | Writes a new value; accepts nullable values for removal |
|
|
45
45
|
| `storageKey` | The key passed to the hook |
|
|
46
46
|
|
|
47
47
|
```ts
|
|
48
48
|
setValue("Andrew");
|
|
49
|
-
setValue(
|
|
49
|
+
setValue(`${value}!`);
|
|
50
|
+
setValue(null); // Removes the key; this hook returns "Guest" again.
|
|
50
51
|
```
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
The hook's setter is typed as `Dispatch<Nullable<T>>`: pass a value directly. Functional updater callbacks are not part of this public hook signature.
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
A non-null fallback selects an overload with a non-null return type. This is a TypeScript declaration, not runtime validation of existing storage data.
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
For object or array fallbacks, reuse a stable reference so that repeated snapshot reads return the same value when the key is missing:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const DEFAULT_PREFERENCES = { theme: "light" as const };
|
|
61
|
+
|
|
62
|
+
// Inside a component:
|
|
63
|
+
const [preferences, setPreferences] = useLocalStorageState<{
|
|
64
|
+
theme: "light" | "dark";
|
|
65
|
+
}>("preferences", DEFAULT_PREFERENCES);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Avoid passing a newly created object or array as the fallback on every render, especially during server rendering and hydration.
|
|
69
|
+
|
|
70
|
+
### `getLocalStorageValue<T>(key, fallback?)`
|
|
57
71
|
|
|
58
72
|
Reads a value without creating a React subscription:
|
|
59
73
|
|
|
60
74
|
```ts
|
|
61
75
|
import { getLocalStorageValue } from "react-use-local-storage";
|
|
62
76
|
|
|
63
|
-
const name = getLocalStorageValue<string>("name");
|
|
77
|
+
const name = getLocalStorageValue<string>("name", "Guest");
|
|
78
|
+
const missing = getLocalStorageValue<string>("missing"); // null if absent
|
|
64
79
|
```
|
|
65
80
|
|
|
66
|
-
Returns `undefined`
|
|
81
|
+
Returns the fallback when the key is missing or `window` is unavailable. An omitted, `null`, or `undefined` fallback is normalized to `null` by the getter. The generic type `T` describes the expected value; it does not validate stored data at runtime.
|
|
67
82
|
|
|
68
|
-
### `setLocalStorageValue<T>(key,
|
|
83
|
+
### `setLocalStorageValue<T>(key, value)`
|
|
69
84
|
|
|
70
85
|
Writes a value and notifies subscribers to that key in the current window:
|
|
71
86
|
|
|
@@ -73,7 +88,7 @@ Writes a value and notifies subscribers to that key in the current window:
|
|
|
73
88
|
import { setLocalStorageValue } from "react-use-local-storage";
|
|
74
89
|
|
|
75
90
|
setLocalStorageValue("name", "Andrew");
|
|
76
|
-
setLocalStorageValue<number>("counter",
|
|
91
|
+
setLocalStorageValue<number>("counter", 1);
|
|
77
92
|
```
|
|
78
93
|
|
|
79
94
|
Writing the same serialized content does not notify subscribers again. Passing `null` removes the value.
|
|
@@ -127,7 +142,7 @@ export const ThemeButton = () => {
|
|
|
127
142
|
return (
|
|
128
143
|
<button
|
|
129
144
|
onClick={() =>
|
|
130
|
-
setPreferences(
|
|
145
|
+
setPreferences({ ...preferences, theme: "dark" })
|
|
131
146
|
}
|
|
132
147
|
>
|
|
133
148
|
Theme: {preferences?.theme ?? "light"}
|
|
@@ -138,6 +153,19 @@ export const ThemeButton = () => {
|
|
|
138
153
|
|
|
139
154
|
When reading data written by other code, the getter reads the `.value` property of parsed JSON. If parsing or subsequent processing throws, it returns the original nonempty string. Arbitrary JSON without the wrapper is not the library's storage format.
|
|
140
155
|
|
|
156
|
+
The fallback does not replace all invalid stored data:
|
|
157
|
+
|
|
158
|
+
| Stored content | Getter result |
|
|
159
|
+
| --- | --- |
|
|
160
|
+
| Missing key | Fallback, or `null` by default |
|
|
161
|
+
| `{"value":42}` | `42` |
|
|
162
|
+
| `{}` | `undefined`, even with a fallback |
|
|
163
|
+
| `{"value":null}` | `null`, even with a non-null fallback |
|
|
164
|
+
| Nonempty invalid JSON | The original raw string |
|
|
165
|
+
| Empty string | Fallback |
|
|
166
|
+
|
|
167
|
+
The getter currently returns `parsedData` directly after reading `.value`. Therefore, the non-null fallback overload does not guarantee a non-null runtime result for arbitrary existing data.
|
|
168
|
+
|
|
141
169
|
## Synchronization
|
|
142
170
|
|
|
143
171
|
- In the current window, use the hook's setter or the library utilities to notify subscribers.
|
|
@@ -149,9 +177,9 @@ When writing directly from another tab, use the `JSON.stringify({ value: ... })`
|
|
|
149
177
|
|
|
150
178
|
## Server-side rendering
|
|
151
179
|
|
|
152
|
-
The hook's server snapshot is `
|
|
180
|
+
The hook's server snapshot is its fallback (`null` by default). When `window` is unavailable, the getter returns its fallback, and writes and clearing are no-ops. During hydration, use the same fallback on the server and client. After hydration, React uses the client snapshot from `localStorage`.
|
|
153
181
|
|
|
154
|
-
|
|
182
|
+
The fallback does not initialize or overwrite stored data.
|
|
155
183
|
|
|
156
184
|
## Limitations
|
|
157
185
|
|
|
@@ -194,4 +222,4 @@ This command builds the package and then publishes it with public access.
|
|
|
194
222
|
|
|
195
223
|
## License
|
|
196
224
|
|
|
197
|
-
MPL-2.0. Author: akb2.
|
|
225
|
+
MPL-2.0. Author: akb2.
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Nullable } from "@akb2/types-tools";
|
|
2
2
|
import { addListenerByKey } from "@utils/add-listener-by-key";
|
|
3
3
|
import { deleteListenerByKey } from "@utils/delete-listener-by-key";
|
|
4
4
|
import { getLocalStorageValue } from "@utils/get-local-storage-value";
|
|
5
|
-
import { setLocalStorageValue } from
|
|
6
|
-
import type { Dispatch
|
|
7
|
-
import { useCallback, useSyncExternalStore } from
|
|
5
|
+
import { setLocalStorageValue } from "@utils/set-local-storage-value";
|
|
6
|
+
import type { Dispatch } from "react";
|
|
7
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* A custom React hook that synchronizes a state variable with local storage.
|
|
@@ -13,11 +13,23 @@ import { useCallback, useSyncExternalStore } from 'react';
|
|
|
13
13
|
* @param key The key in local storage to associate with the state variable.
|
|
14
14
|
* @returns A tuple containing the state variable, a setter function, and the key.
|
|
15
15
|
*/
|
|
16
|
-
export
|
|
16
|
+
export function useLocalStorageState<T>(key: string): [Nullable<T>, Dispatch<Nullable<T>>, string];
|
|
17
|
+
export function useLocalStorageState<T>(
|
|
18
|
+
key: string,
|
|
19
|
+
fallback: Exclude<T, null | undefined>,
|
|
20
|
+
): [Exclude<T, null | undefined>, Dispatch<Nullable<T>>, string];
|
|
21
|
+
export function useLocalStorageState<T>(
|
|
22
|
+
key: string,
|
|
23
|
+
fallback: Nullable<T>,
|
|
24
|
+
): [Nullable<T>, Dispatch<Nullable<T>>, string];
|
|
25
|
+
export function useLocalStorageState<T>(
|
|
26
|
+
key: string,
|
|
27
|
+
fallback: Nullable<T> = null,
|
|
28
|
+
): [Nullable<T>, Dispatch<Nullable<T>>, string] {
|
|
17
29
|
const setState = useCallback(setLocalStorageValue.bind(null, key), [key]);
|
|
18
30
|
const state = useSyncExternalStore(
|
|
19
31
|
(onStoreChange) => {
|
|
20
|
-
if (typeof window ===
|
|
32
|
+
if (typeof window === "undefined") {
|
|
21
33
|
return (): void => {};
|
|
22
34
|
}
|
|
23
35
|
|
|
@@ -25,9 +37,9 @@ export const useLocalStorageState = <T>(key: string): [NotDefinable<T>, Dispatch
|
|
|
25
37
|
|
|
26
38
|
return (): void => deleteListenerByKey(key, onStoreChange);
|
|
27
39
|
},
|
|
28
|
-
() => getLocalStorageValue<T>(key) as T,
|
|
29
|
-
() =>
|
|
40
|
+
() => getLocalStorageValue<T>(key, fallback) as T,
|
|
41
|
+
() => fallback,
|
|
30
42
|
);
|
|
31
43
|
|
|
32
44
|
return [state, setState, key];
|
|
33
|
-
}
|
|
45
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isDefined,
|
|
1
|
+
import { isDefined, Nullable } from "@akb2/types-tools";
|
|
2
2
|
import { deepFreeze } from "@utils/deep-freeze";
|
|
3
3
|
import { getOriginalDataStorageKey } from "./get-original-data-storage-key";
|
|
4
4
|
|
|
@@ -9,22 +9,33 @@ import { getOriginalDataStorageKey } from "./get-original-data-storage-key";
|
|
|
9
9
|
* @param key The key in local storage to retrieve the value for.
|
|
10
10
|
* @returns The value associated with the key, or undefined if not found.
|
|
11
11
|
*/
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
export function getLocalStorageValue<T>(key: string): Nullable<T>;
|
|
13
|
+
export function getLocalStorageValue<T>(
|
|
14
|
+
key: string,
|
|
15
|
+
fallback: Exclude<T, null | undefined>,
|
|
16
|
+
): Exclude<T, null | undefined>;
|
|
17
|
+
export function getLocalStorageValue<T>(key: string, fallback: Nullable<T>): Nullable<T>;
|
|
18
|
+
export function getLocalStorageValue<T>(
|
|
19
|
+
key: string,
|
|
20
|
+
mixedFallback: Nullable<T> = null,
|
|
21
|
+
): Nullable<T> {
|
|
22
|
+
const fallback = mixedFallback ?? null;
|
|
23
|
+
|
|
24
|
+
if (typeof window === "undefined") {
|
|
25
|
+
return fallback;
|
|
15
26
|
}
|
|
16
27
|
|
|
17
28
|
const raw = localStorage.getItem(key);
|
|
18
29
|
|
|
19
30
|
if (!isDefined(raw)) {
|
|
20
|
-
return
|
|
31
|
+
return fallback;
|
|
21
32
|
}
|
|
22
33
|
|
|
23
|
-
if(!isDefined(window.__AKB2_LOCAL_STORAGE__)){
|
|
24
|
-
window.__AKB2_LOCAL_STORAGE__ = {
|
|
34
|
+
if (!isDefined(window.__AKB2_LOCAL_STORAGE__)) {
|
|
35
|
+
window.__AKB2_LOCAL_STORAGE__ = {} as typeof window.__AKB2_LOCAL_STORAGE__;
|
|
25
36
|
}
|
|
26
37
|
|
|
27
|
-
if(!isDefined(window.__AKB2_LOCAL_STORAGE__.originalData)){
|
|
38
|
+
if (!isDefined(window.__AKB2_LOCAL_STORAGE__.originalData)) {
|
|
28
39
|
window.__AKB2_LOCAL_STORAGE__.originalData = new Map();
|
|
29
40
|
}
|
|
30
41
|
|
|
@@ -44,6 +55,6 @@ export const getLocalStorageValue = <T>(key: string): NotDefinable<T> => {
|
|
|
44
55
|
|
|
45
56
|
return parsedData as Readonly<T>;
|
|
46
57
|
} catch {
|
|
47
|
-
return raw.length > 0 ? (raw as unknown as T) :
|
|
58
|
+
return raw.length > 0 ? (raw as unknown as T) : fallback;
|
|
48
59
|
}
|
|
49
|
-
}
|
|
60
|
+
}
|
|
@@ -14,14 +14,14 @@ describe("LocalStorage direct changing", () => {
|
|
|
14
14
|
it("No support changes with direct localStorage modification", () => {
|
|
15
15
|
const { result } = renderHook(() => useLocalStorageState("initial"));
|
|
16
16
|
|
|
17
|
-
expect(result.current[0]).
|
|
17
|
+
expect(result.current[0]).toBeNull();
|
|
18
18
|
|
|
19
19
|
localStorage.setItem(
|
|
20
20
|
"initial",
|
|
21
21
|
JSON.stringify({ value: "new value with direct modification" }),
|
|
22
22
|
);
|
|
23
23
|
|
|
24
|
-
return waitFor(() => expect(result.current[0]).
|
|
24
|
+
return waitFor(() => expect(result.current[0]).toBeNull(), {
|
|
25
25
|
timeout: 2000,
|
|
26
26
|
});
|
|
27
27
|
});
|
|
@@ -14,13 +14,13 @@ describe("A simple value changing", () => {
|
|
|
14
14
|
it("No changing a value", () => {
|
|
15
15
|
const { result } = renderHook(() => useLocalStorageState("initial"));
|
|
16
16
|
|
|
17
|
-
expect(result.current[0]).
|
|
17
|
+
expect(result.current[0]).toBeNull();
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
it("Changing a value", () => {
|
|
21
21
|
const { result } = renderHook(() => useLocalStorageState("initial"));
|
|
22
22
|
|
|
23
|
-
expect(result.current[0]).
|
|
23
|
+
expect(result.current[0]).toBeNull();
|
|
24
24
|
act(() => result.current[1]("new value"));
|
|
25
25
|
expect(result.current[0]).toBe("new value");
|
|
26
26
|
});
|
|
@@ -30,10 +30,10 @@ describe("A simple value changing", () => {
|
|
|
30
30
|
|
|
31
31
|
const { result } = renderHook(() => useLocalStorageState("initial"));
|
|
32
32
|
|
|
33
|
-
expect(result.current[0]).
|
|
33
|
+
expect(result.current[0]).toBeNull();
|
|
34
34
|
setTimeout(() => result.current[1]("new value with timeout"), 100);
|
|
35
35
|
act(() => vi.advanceTimersByTime(99));
|
|
36
|
-
expect(result.current[0]).
|
|
36
|
+
expect(result.current[0]).toBeNull();
|
|
37
37
|
act(() => vi.advanceTimersByTime(1));
|
|
38
38
|
expect(result.current[0]).toBe("new value with timeout");
|
|
39
39
|
});
|