Skip to content

Commit bcb3fc9

Browse files
committed
feat: Added use-local-storage store.
1 parent f45c011 commit bcb3fc9

File tree

5 files changed

+232
-100
lines changed

5 files changed

+232
-100
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ Based on the [@mantine/hooks](https://github.com/mantinedev/mantine/tree/master/
8888
- [x] ~~use-isomorphic-effect~~ (Solid's [`createEffect`](https://docs.solidjs.com/reference/basic-reactivity/create-effect) is technically already isomorphic because it doesn't error on SSR. Also, it also only runs on client-side.)
8989
- [ ] use-list-state
9090
- [x] use-local-storage
91+
- [x] use-local-storage-store (✨ Improved, more similar to 'createStore' API).
9192
- [ ] use-logger
9293
- [ ] use-map
9394
- [x] use-media-query

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export * from './use-in-viewport/use-in-viewport';
2020
export * from './use-input-state/use-input-state';
2121
export * from './use-intersection/use-intersection';
2222
export * from './use-local-storage/use-local-storage';
23+
export * from './use-local-storage/use-local-storage-store';
2324
export * from './use-media-query/use-media-query';
2425
export * from './use-mounted/use-mounted';
2526
export * from './use-mouse/use-mouse';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// ./create-storage.ts
2+
3+
import { createEffect, onCleanup, onMount } from 'solid-js';
4+
import { createStore, reconcile, StoreSetter, unwrap } from 'solid-js/store';
5+
import {
6+
_createStorageHandler,
7+
_deserializeJSON,
8+
_serializeJSON,
9+
readLocalStorageValue,
10+
readValue,
11+
StorageProperties,
12+
StorageType,
13+
} from './utils';
14+
15+
export { readLocalStorageValue, readValue };
16+
export type { StorageProperties, StorageType };
17+
18+
export function createStorageStore<T extends Object>(type: StorageType, hookName: string) {
19+
const eventName =
20+
type === 'localStorage' ? 'bagon-local-storage-store' : 'bagon-session-storage-store';
21+
const { getItem, setItem, removeItem } = _createStorageHandler(type);
22+
23+
return function useStorage({
24+
key,
25+
defaultValue,
26+
getInitialValueInEffect = true,
27+
deserialize = _deserializeJSON,
28+
serialize = (value: T) => _serializeJSON(value, hookName),
29+
}: StorageProperties<T>) {
30+
const readStorageValue = (skipStorage?: boolean): T => {
31+
let storageBlockedOrSkipped;
32+
33+
try {
34+
storageBlockedOrSkipped =
35+
typeof window === 'undefined' ||
36+
!(type in window) ||
37+
window[type] === null ||
38+
!!skipStorage;
39+
} catch (_e) {
40+
storageBlockedOrSkipped = true;
41+
}
42+
43+
if (storageBlockedOrSkipped) {
44+
return defaultValue as T;
45+
}
46+
47+
const storageValue = getItem(key);
48+
return storageValue !== null ? deserialize(storageValue) : (defaultValue as T);
49+
};
50+
51+
const [value, setValue] = createStore<T>(readStorageValue(getInitialValueInEffect));
52+
53+
const setStorageValue = (setter: StoreSetter<T>) => {
54+
setValue(setter);
55+
56+
const val = unwrap(value);
57+
setItem(key, serialize(val));
58+
window.dispatchEvent(new CustomEvent(eventName, { detail: { key, value: val } }));
59+
};
60+
61+
const removeStorageValue = () => {
62+
removeItem(key);
63+
window.dispatchEvent(new CustomEvent(eventName, { detail: { key, value: defaultValue } }));
64+
};
65+
66+
// (Replaced useWindowEvent with this)
67+
onMount(() => {
68+
// 1. Storage Event
69+
const storageListener = (event: StorageEvent) => {
70+
if (event.storageArea === window[type] && event.key === key) {
71+
setValue(reconcile(deserialize(event.newValue ?? undefined) as any));
72+
}
73+
};
74+
window.addEventListener('storage', storageListener);
75+
76+
// 2. Custom Event (So the stored value is reactive)
77+
const customEventListener = (event: any) => {
78+
if (event.detail.key === key) {
79+
setValue(reconcile(event.detail.value));
80+
}
81+
};
82+
window.addEventListener(eventName as any, storageListener);
83+
84+
onCleanup(() => {
85+
window.removeEventListener('storage', storageListener);
86+
window.removeEventListener(eventName, customEventListener);
87+
});
88+
});
89+
90+
createEffect(() => {
91+
if (defaultValue !== undefined && value === undefined) {
92+
setStorageValue(reconcile(defaultValue));
93+
}
94+
});
95+
96+
onMount(() => {
97+
const val = readStorageValue();
98+
val !== undefined && setStorageValue(val);
99+
});
100+
101+
return [value === undefined ? defaultValue : value, setStorageValue, removeStorageValue] as [
102+
T,
103+
typeof setStorageValue,
104+
typeof removeStorageValue,
105+
];
106+
};
107+
}
108+
109+
// ./use-local-storage.ts
110+
111+
/**
112+
* An improved hook similar to `useLocalStorage` that allows using value from the localStorage as a signal.
113+
* The hook works the same way as createStore, but also writes the value to the localStorage.
114+
*
115+
* It's also reactive across different pages.
116+
*/
117+
export function useLocalStorageStore<T extends Object>(props: StorageProperties<T>) {
118+
return createStorageStore<T>('localStorage', 'use-local-storage')(props);
119+
}
+18-100
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,29 @@
11
// ./create-storage.ts
22

33
import { Accessor, createEffect, createSignal, onCleanup, onMount } from 'solid-js';
4-
5-
export type StorageType = 'localStorage' | 'sessionStorage';
6-
7-
export interface StorageProperties<T> {
8-
/** Storage key */
9-
key: string;
10-
11-
/** Default value that will be set if value is not found in storage */
12-
defaultValue?: T;
13-
14-
/** If set to true, value will be updated in createEffect after mount. Default value is true. */
15-
getInitialValueInEffect?: boolean;
16-
17-
/** Function to serialize value into string to be save in storage */
18-
serialize?: (value: T) => string;
19-
20-
/** Function to deserialize string value from storage to value */
21-
deserialize?: (value: string | undefined) => T;
22-
}
23-
24-
function serializeJSON<T>(value: T, hookName: string = 'use-local-storage') {
25-
try {
26-
return JSON.stringify(value);
27-
} catch (error) {
28-
throw new Error(`bagon-hooks ${hookName}: Failed to serialize the value`);
29-
}
30-
}
31-
32-
function deserializeJSON(value: string | undefined) {
33-
try {
34-
return value && JSON.parse(value);
35-
} catch {
36-
return value;
37-
}
38-
}
39-
40-
function createStorageHandler(type: StorageType) {
41-
const getItem = (key: string) => {
42-
try {
43-
return window[type].getItem(key);
44-
} catch (error) {
45-
console.warn('use-local-storage: Failed to get value from storage, localStorage is blocked');
46-
return null;
47-
}
48-
};
49-
50-
const setItem = (key: string, value: string) => {
51-
try {
52-
window[type].setItem(key, value);
53-
} catch (error) {
54-
console.warn('use-local-storage: Failed to set value to storage, localStorage is blocked');
55-
}
56-
};
57-
58-
const removeItem = (key: string) => {
59-
try {
60-
window[type].removeItem(key);
61-
} catch (error) {
62-
console.warn(
63-
'use-local-storage: Failed to remove value from storage, localStorage is blocked',
64-
);
65-
}
66-
};
67-
68-
return { getItem, setItem, removeItem };
69-
}
4+
import {
5+
_createStorageHandler,
6+
_deserializeJSON,
7+
_serializeJSON,
8+
readLocalStorageValue,
9+
readValue,
10+
StorageProperties,
11+
StorageType,
12+
} from './utils';
13+
14+
export { readLocalStorageValue, readValue };
15+
export type { StorageProperties, StorageType };
7016

7117
export function createStorage<T>(type: StorageType, hookName: string) {
7218
const eventName = type === 'localStorage' ? 'bagon-local-storage' : 'bagon-session-storage';
73-
const { getItem, setItem, removeItem } = createStorageHandler(type);
19+
const { getItem, setItem, removeItem } = _createStorageHandler(type);
7420

7521
return function useStorage({
7622
key,
7723
defaultValue,
7824
getInitialValueInEffect = true,
79-
deserialize = deserializeJSON,
80-
serialize = (value: T) => serializeJSON(value, hookName),
25+
deserialize = _deserializeJSON,
26+
serialize = (value: T) => _serializeJSON(value, hookName),
8127
}: StorageProperties<T>) {
8228
const readStorageValue = (skipStorage?: boolean): T => {
8329
let storageBlockedOrSkipped;
@@ -149,10 +95,10 @@ export function createStorage<T>(type: StorageType, hookName: string) {
14995
});
15096

15197
createEffect(() => {
152-
if (defaultValue !== undefined && value === undefined) {
98+
if (defaultValue !== undefined && value() === undefined) {
15399
setStorageValue(defaultValue);
154100
}
155-
}, [defaultValue, value, setStorageValue]);
101+
});
156102

157103
onMount(() => {
158104
const val = readStorageValue();
@@ -167,32 +113,6 @@ export function createStorage<T>(type: StorageType, hookName: string) {
167113
};
168114
}
169115

170-
export function readValue(type: StorageType) {
171-
const { getItem } = createStorageHandler(type);
172-
173-
return function read<T>({
174-
key,
175-
defaultValue,
176-
deserialize = deserializeJSON,
177-
}: StorageProperties<T>) {
178-
let storageBlockedOrSkipped;
179-
180-
try {
181-
storageBlockedOrSkipped =
182-
typeof window === 'undefined' || !(type in window) || window[type] === null;
183-
} catch (_e) {
184-
storageBlockedOrSkipped = true;
185-
}
186-
187-
if (storageBlockedOrSkipped) {
188-
return defaultValue as T;
189-
}
190-
191-
const storageValue = getItem(key);
192-
return storageValue !== null ? deserialize(storageValue) : (defaultValue as T);
193-
};
194-
}
195-
196116
// ./use-local-storage.ts
197117

198118
/**
@@ -201,8 +121,6 @@ export function readValue(type: StorageType) {
201121
*
202122
* It's also reactive across different pages.
203123
*/
204-
export function useLocalStorage<T = string>(props: StorageProperties<T>) {
124+
export function useLocalStorage<T>(props: StorageProperties<T>) {
205125
return createStorage<T>('localStorage', 'use-local-storage')(props);
206126
}
207-
208-
export const readLocalStorageValue = readValue('localStorage');

src/use-local-storage/utils.ts

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
export type StorageType = 'localStorage' | 'sessionStorage';
2+
3+
export interface StorageProperties<T> {
4+
/** Storage key */
5+
key: string;
6+
7+
/** Default value that will be set if value is not found in storage */
8+
defaultValue?: T;
9+
10+
/** If set to true, value will be updated in createEffect after mount. Default value is true. */
11+
getInitialValueInEffect?: boolean;
12+
13+
/** Function to serialize value into string to be save in storage */
14+
serialize?: (value: T) => string;
15+
16+
/** Function to deserialize string value from storage to value */
17+
deserialize?: (value: string | undefined) => T;
18+
}
19+
20+
export function _serializeJSON<T>(value: T, hookName: string = 'use-local-storage') {
21+
try {
22+
return JSON.stringify(value);
23+
} catch (error) {
24+
throw new Error(`bagon-hooks ${hookName}: Failed to serialize the value`);
25+
}
26+
}
27+
28+
export function _deserializeJSON(value: string | undefined) {
29+
try {
30+
return value && JSON.parse(value);
31+
} catch {
32+
return value;
33+
}
34+
}
35+
36+
export function _createStorageHandler(type: StorageType) {
37+
const getItem = (key: string) => {
38+
try {
39+
return window[type].getItem(key);
40+
} catch (error) {
41+
console.warn('use-local-storage: Failed to get value from storage, localStorage is blocked');
42+
return null;
43+
}
44+
};
45+
46+
const setItem = (key: string, value: string) => {
47+
try {
48+
window[type].setItem(key, value);
49+
} catch (error) {
50+
console.warn('use-local-storage: Failed to set value to storage, localStorage is blocked');
51+
}
52+
};
53+
54+
const removeItem = (key: string) => {
55+
try {
56+
window[type].removeItem(key);
57+
} catch (error) {
58+
console.warn(
59+
'use-local-storage: Failed to remove value from storage, localStorage is blocked',
60+
);
61+
}
62+
};
63+
64+
return { getItem, setItem, removeItem };
65+
}
66+
67+
export function readValue(type: StorageType) {
68+
const { getItem } = _createStorageHandler(type);
69+
70+
return function read<T>({
71+
key,
72+
defaultValue,
73+
deserialize = _deserializeJSON,
74+
}: StorageProperties<T>) {
75+
let storageBlockedOrSkipped;
76+
77+
try {
78+
storageBlockedOrSkipped =
79+
typeof window === 'undefined' || !(type in window) || window[type] === null;
80+
} catch (_e) {
81+
storageBlockedOrSkipped = true;
82+
}
83+
84+
if (storageBlockedOrSkipped) {
85+
return defaultValue as T;
86+
}
87+
88+
const storageValue = getItem(key);
89+
return storageValue !== null ? deserialize(storageValue) : (defaultValue as T);
90+
};
91+
}
92+
93+
export const readLocalStorageValue = readValue('localStorage');

0 commit comments

Comments
 (0)