diff --git a/packages/runed/src/lib/utilities/persisted-state/persisted-state.svelte.ts b/packages/runed/src/lib/utilities/persisted-state/persisted-state.svelte.ts index 6d03c747..081b255c 100644 --- a/packages/runed/src/lib/utilities/persisted-state/persisted-state.svelte.ts +++ b/packages/runed/src/lib/utilities/persisted-state/persisted-state.svelte.ts @@ -96,6 +96,7 @@ function proxy( * @see {@link https://runed.dev/docs/utilities/persisted-state} */ export class PersistedState { + #initial: T; #current: T | undefined; #key: string; #serializer: Serializer; @@ -118,7 +119,7 @@ export class PersistedState { } = options; const window = "window" in options ? options.window : defaultWindow; // window is not mockable to be undefined without this, because JavaScript will fill undefined with `= default` - this.#current = initialValue; + this.#initial = this.#current = initialValue; this.#key = key; this.#serializer = serializer; this.#connected = connected; @@ -168,6 +169,20 @@ export class PersistedState { this.#update?.(); } + /** + * Resets the state back to the initial value provided to the constructor. + * + * When connected to storage, an initial value of `undefined` results in the key + * being removed from storage. + * (If the initial value is `null`, it will be persisted as the string `"null"` + * by the default JSON serializer.) + */ + reset() { + this.#current = this.#initial; + this.#serialize(this.#initial); + this.#update?.(); + } + #handleStorageEvent = (event: StorageEvent): void => { if (event.key !== this.#key || event.newValue === null) return; this.#current = this.#deserialize(event.newValue); @@ -193,6 +208,9 @@ export class PersistedState { try { if (value !== undefined) { this.#storage?.setItem(this.#key, this.#serializer.serialize(value)); + } else { + this.#current = value; + this.#storage?.removeItem(this.#key); } } catch (error) { console.error( diff --git a/packages/runed/src/lib/utilities/persisted-state/persisted-state.test.svelte.ts b/packages/runed/src/lib/utilities/persisted-state/persisted-state.test.svelte.ts index bfc0e74a..0b310079 100644 --- a/packages/runed/src/lib/utilities/persisted-state/persisted-state.test.svelte.ts +++ b/packages/runed/src/lib/utilities/persisted-state/persisted-state.test.svelte.ts @@ -103,6 +103,36 @@ describe("PersistedState", async () => { expect(persistedState.current).toBe(initialValue); }); + testWithEffect("removes the key when set to undefined", () => { + const persistedState = new PersistedState(key, initialValue); + expect(localStorage.getItem(key)).toBe(JSON.stringify(initialValue)); + + persistedState.current = undefined; + expect(persistedState.current).toBeUndefined(); + expect(localStorage.getItem(key)).toBeNull(); + }); + + testWithEffect("reset restores the initial value after modifications", () => { + const persistedState = new PersistedState(key, initialValue); + persistedState.current = newValue; + expect(persistedState.current).toBe(newValue); + expect(localStorage.getItem(key)).toBe(JSON.stringify(newValue)); + + persistedState.reset(); + expect(persistedState.current).toBe(initialValue); + expect(localStorage.getItem(key)).toBe(JSON.stringify(initialValue)); + }); + + testWithEffect("reset removes the key if initial value is undefined", () => { + localStorage.setItem(key, JSON.stringify(existingValue)); + const persistedState = new PersistedState(key, undefined); + expect(localStorage.getItem(key)).toBe(JSON.stringify(existingValue)); + + persistedState.reset(); + expect(persistedState.current).toBeUndefined(); + expect(localStorage.getItem(key)).toBeNull(); + }); + testWithEffect("uses persisted value if it is found", () => { localStorage.setItem(key, JSON.stringify(existingValue)); const persistedState = new PersistedState(key, initialValue); diff --git a/sites/docs/src/content/utilities/persisted-state.md b/sites/docs/src/content/utilities/persisted-state.md index f3b868f8..ec65f8cc 100644 --- a/sites/docs/src/content/utilities/persisted-state.md +++ b/sites/docs/src/content/utilities/persisted-state.md @@ -41,6 +41,28 @@ Initialize `PersistedState` by providing a unique key and an initial value for t ``` +### Reset + +You can reset back to the initial value passed to the constructor: + +```ts +const state = new PersistedState("count", 0); + +state.current = 123; +state.reset(); +console.log(state.current); // 0 +``` + +### Clearing persisted storage + +When connected to storage, setting `current` to `undefined` removes the key from storage: + +```ts +const state = new PersistedState("draft", "hello"); + +state.current = undefined; // removes "draft" from storage +``` + ### Complex objects When persisting complex objects, only plain structures are deeply reactive.