Skip to content
15 changes: 8 additions & 7 deletions docs/composable/storage/localStorage.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
```js
import { useLocalStorage } from "vue-composable";

const localStorage = useLocalStorage(key, defaultValue?, sync?);
const localStorage = useLocalStorage<T=string>(key, defaultValue?, sync?, useDebounce?);
```

| Parameters | Type | Required | Default | Description |
| ------------ | --------------------- | -------- | ----------- | --------------------------------------------------- |
| key | `string, ref<string>` | `true` | | Key that will be used to store in localStorage |
| defaultValue | `object` | `false` | `undefined` | default value stored in the localStorage |
| key | `string, Ref<string>` | `true` | | Key that will be used to store in localStorage |
| defaultValue | `T, Ref<T>` | `false` | `undefined` | default value stored in the localStorage |
| sync | `Boolean` | `false` | `true` | sets the storage to sync automatically between tabs |
| useDebounce | `Boolean` | `false` | `true` | updates value in localStorage once every 10ms |

## State

Expand All @@ -26,10 +27,10 @@ import { useLocalStorage } from "vue-composable";
const { supported, storage } = useLocalStorage(key);
```

| State | Type | Description |
| --------- | ---------- | ------------------------------------------- |
| supported | `boolean` | returns true is `localStorage` is available |
| storage | `Ref<any>` | handler with localStorage value |
| State | Type | Description |
| --------- | --------------------- | ------------------------------------------- |
| supported | `boolean` | returns true is `localStorage` is available |
| storage | `Ref<T \| undefined>` | handler with localStorage value |

## Methods

Expand Down
31 changes: 15 additions & 16 deletions docs/composable/storage/sessionStorage.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
```js
import { useSessionStorage } from "vue-composable";

const SessionStorage = useSessionStorage(key, defaultValue?, sync?);
const SessionStorage = useSessionStorage<T=string>(key, defaultValue?, useDebounce?);
```

| Parameters | Type | Required | Default | Description |
| ------------ | --------------------- | -------- | ----------- | --------------------------------------------------- |
| key | `string, ref<string>` | `true` | | Key that will be used to store in SessionStorage |
| defaultValue | `object` | `false` | `undefined` | default value stored in the SessionStorage |
| sync | `Boolean` | `false` | `true` | sets the storage to sync automatically between tabs |
| Parameters | Type | Required | Default | Description |
| ------------ | --------------------- | -------- | ----------- | ------------------------------------------------ |
| key | `string, Ref<string>` | `true` | | Key that will be used to store in SessionStorage |
| defaultValue | `T, Ref<T>` | `false` | `undefined` | default value stored in the SessionStorage |
| useDebounce | `Boolean` | `false` | `true` | updates value in sessionStorage once every 10ms |

## State

Expand All @@ -26,10 +26,10 @@ import { useSessionStorage } from "vue-composable";
const { supported, storage } = useSessionStorage(key);
```

| State | Type | Description |
| --------- | ---------- | --------------------------------------------- |
| supported | `boolean` | returns true is `SessionStorage` is available |
| storage | `Ref<any>` | handler with SessionStorage value |
| State | Type | Description |
| --------- | --------------------- | --------------------------------------------- |
| supported | `boolean` | returns true is `SessionStorage` is available |
| storage | `Ref<T \| undefined>` | handler with SessionStorage value |

## Methods

Expand All @@ -38,14 +38,13 @@ The `useSessionStorage` function exposes the following methods:
```js
import { useSessionStorage } from "vue-composable";

const { remove, clear, setSync } = useSessionStorage(key);
const { remove, clear } = useSessionStorage(key);
```

| Signature | Description |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `remove()` | Removes key from the SessionStorage, equivalent as `storage.value = undefined` |
| `clear()` | Clears all used SessionStorage used so far |
| `setSync(boolean)` | Does nothing, since the session is only available on the tab, this is here to allow the same API as `useLocalStorage`. Returns `false` |
| Signature | Description |
| ---------- | ------------------------------------------------------------------------------ |
| `remove()` | Removes key from the SessionStorage, equivalent as `storage.value = undefined` |
| `clear()` | Clears all used SessionStorage used so far |

## Example

Expand Down
58 changes: 55 additions & 3 deletions packages/vue-composable/__tests__/storage/localStorage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,57 @@ describe("localStorage", () => {
consoleWarnSpy.mockClear();
});

it("should remove value from localStorage when ref is set to undefined", async () => {
const key = "test";
const value = "value";
const { storage } = useLocalStorage<string | undefined>(key, value);

await nextTick();

expect(storage.value).toEqual(value);
expect(localStorage.getItem(key)).toEqual(value);

storage.value = undefined;

await promisedTimeout(100);

expect(storage.value).toEqual(undefined);
expect(localStorage.getItem(key)).toEqual(null);
});

it("should not set value in localStorage when defaultValue is undefined", async () => {
const key = "test";
const { storage } = useLocalStorage(key);

await nextTick();

expect(storage.value).toEqual(undefined);
expect(localStorage.getItem(key)).toEqual(null);
});

it("should handle ref value", async () => {
const key = "test";
const value = ref(5);
const { storage } = useLocalStorage(key, value);

await nextTick();

expect(storage.value).toEqual(value.value);
expect(localStorage.getItem(key)).toEqual(JSON.stringify(value.value));
});

it("should update localStorage immediately if we\re not using debounce", async () => {
const key = "test";
let value = 5;
const { storage } = useLocalStorage(key, value, false, false);

expect(localStorage.getItem(key)).toEqual(JSON.stringify(value));

storage.value = 10;

expect(localStorage.getItem(key)).toEqual(JSON.stringify(storage.value));
});

it("should store object in localStorage if default is passed", async () => {
const obj = { a: 1 };
const { storage } = useLocalStorage("test", obj);
Expand All @@ -32,7 +83,7 @@ describe("localStorage", () => {
expect(storage.value).toMatchObject(obj);
expect(setItemSpy).toHaveBeenLastCalledWith("test", JSON.stringify(obj));

storage.value.a = 33;
storage.value!.a = 33;
await nextTick();

expect(storage.value).toMatchObject({ a: 33 });
Expand All @@ -48,6 +99,7 @@ describe("localStorage", () => {
const { storage: storage1 } = useLocalStorage(key, { a: 1 });
const { storage: storage2 } = useLocalStorage(key, { b: 1 });

// @ts-ignore
expect(storage1.value).toMatchObject(storage2.value);
});

Expand Down Expand Up @@ -127,8 +179,8 @@ describe("localStorage", () => {

// key not created yet
// expect(localStorage.length).toBe(1);
// key doesn't exist so it's null
expect(storage.value).toBeNull();
// key doesn't exist so it's undefined
expect(storage.value).toBeUndefined();
jest.runAllTimers();

storage.value = { k: 0 };
Expand Down
69 changes: 53 additions & 16 deletions packages/vue-composable/__tests__/storage/sessionStorage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,55 @@ describe("sessionStorage", () => {
consoleWarnSpy.mockClear();
});

it("should remove value from sessionStorage when ref is set to undefined", async () => {
const key = "test";
const value = "value";
const { storage } = useSessionStorage<string | undefined>(key, value);

expect(storage.value).toEqual(value);
expect(sessionStorage.getItem(key)).toEqual(value);

storage.value = undefined;

await promisedTimeout(100);

expect(storage.value).toEqual(undefined);
expect(sessionStorage.getItem(key)).toEqual(null);
});

it("should not set value in sessionStorage when defaultValue is undefined", async () => {
const key = "test";
const { storage } = useSessionStorage(key);

await nextTick();

expect(storage.value).toEqual(undefined);
expect(sessionStorage.getItem(key)).toEqual(null);
});

it("should handle ref value", async () => {
const key = "test";
const value = ref(5);
const { storage } = useSessionStorage(key, value);

await nextTick();

expect(storage.value).toEqual(value.value);
expect(sessionStorage.getItem(key)).toEqual(JSON.stringify(value.value));
});

it("should update sessionStorage immediately if we\re not using debounce", async () => {
const key = "test";
let value = 5;
const { storage } = useSessionStorage(key, value, false);

expect(sessionStorage.getItem(key)).toEqual(JSON.stringify(value));

storage.value = 10;

expect(sessionStorage.getItem(key)).toEqual(JSON.stringify(storage.value));
});

it("should store object in sessionStorage if default is passed", async () => {
const obj = { a: 1 };
const { storage } = useSessionStorage("test", obj);
Expand All @@ -32,7 +81,7 @@ describe("sessionStorage", () => {
expect(storage.value).toMatchObject(obj);
expect(setItemSpy).toHaveBeenLastCalledWith("test", JSON.stringify(obj));

storage.value.a = 33;
storage.value!.a = 33;
await nextTick();

expect(storage.value).toMatchObject({ a: 33 });
Expand All @@ -48,6 +97,7 @@ describe("sessionStorage", () => {
const { storage: storage1 } = useSessionStorage(key, { a: 1 });
const { storage: storage2 } = useSessionStorage(key, { b: 1 });

// @ts-ignore
expect(storage1.value).toMatchObject(storage2.value);
});

Expand Down Expand Up @@ -93,17 +143,6 @@ describe("sessionStorage", () => {
expect(storage.value).toMatchObject({ k: 1 });
});

it("should warn if you try to sync", () => {
const key = "hello";
const { setSync } = useSessionStorage(key, { k: 10 });

expect(consoleWarnSpy).not.toHaveBeenCalled();
setSync(true);
expect(consoleWarnSpy).toHaveBeenCalledWith(
"sync is not supported, please `useLocalStorage` instead"
);
});

it("should warn if sessionStorage is not supported", () => {
setItemSpy.mockImplementationOnce(() => {
throw new Error("random");
Expand All @@ -127,10 +166,8 @@ describe("sessionStorage", () => {
key.value = "hey";
jest.runAllTimers();

// key not created yet
// expect(localStorage.length).toBe(1);
// key doesn't exist so it's null
expect(storage.value).toBeNull();
// key doesn't exist so it's undefined
expect(storage.value).toBeUndefined();
jest.runAllTimers();

storage.value = { k: 0 };
Expand Down
44 changes: 21 additions & 23 deletions packages/vue-composable/src/storage/localStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,61 +3,59 @@ import { RefTyped, NO_OP, unwrap } from "../utils";
import { useWebStorage } from "./webStorage";

export interface LocalStorageReturn<T> {
/**
* returns true is `localStorage` is available
*/
supported: boolean;

storage: Ref<T>;
/**
* handler with `localStorage` value
*/

storage: Ref<T | undefined>;

/**
* @description Removes current item from the store
* Removes current item from the store
*/
remove: () => void;

/**
* @description Clears all tracked localStorage items
* Clears all tracked `localStorage` items
*/
clear: () => void;

/**
* @description Enable cross tab syncing
* Enable cross tab syncing
*/
setSync: (sync: boolean) => void;
}

export function useLocalStorage(
key: RefTyped<string>,
defaultValue?: RefTyped<string>,
sync?: boolean
): LocalStorageReturn<string>;
export function useLocalStorage<T>(
export function useLocalStorage<T = string>(
key: RefTyped<string>,
defaultValue?: RefTyped<T>,
sync?: boolean
): LocalStorageReturn<T>;
export function useLocalStorage(
key: RefTyped<string>,
defaultValue?: RefTyped<any>,
sync?: boolean
) {
sync = true,
useDebounce = true
): LocalStorageReturn<T> {
const { supported, store } = useWebStorage("localStorage");

let remove = NO_OP;
let clear = NO_OP;
let setSync: LocalStorageReturn<any>["setSync"] = NO_OP;
let storage = undefined;
let storage = ref<T>();

if (supported && store) {
setSync = (s) => store.setSync(unwrap(key), s);
remove = () => store.removeItem(unwrap(key));
clear = () => store.clear();

storage = store.getRef(key);
if (storage.value == null) {
storage = store.getRef<T>(key, useDebounce);
if (storage.value === undefined) {
store.save(unwrap(key), defaultValue);
storage.value = defaultValue;
storage.value = unwrap(defaultValue);
}

watchEffect(() => {
if (sync !== false) {
if (sync) {
setSync(true);
}
});
Expand All @@ -67,7 +65,7 @@ export function useLocalStorage(
console.warn("[localStorage] is not available");
}

storage = ref(defaultValue);
storage.value = unwrap(defaultValue);
}

return {
Expand Down
Loading