Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ function proxy<T>(
* @see {@link https://runed.dev/docs/utilities/persisted-state}
*/
export class PersistedState<T> {
#initial: T;
#current: T | undefined;
#key: string;
#serializer: Serializer<T>;
Expand All @@ -118,7 +119,7 @@ export class PersistedState<T> {
} = 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;
Expand Down Expand Up @@ -168,6 +169,20 @@ export class PersistedState<T> {
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);
Expand All @@ -193,6 +208,9 @@ export class PersistedState<T> {
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,36 @@ describe("PersistedState", async () => {
expect(persistedState.current).toBe(initialValue);
});

testWithEffect("removes the key when set to undefined", () => {
const persistedState = new PersistedState<string | undefined>(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<string | undefined>(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);
Expand Down
22 changes: 22 additions & 0 deletions sites/docs/src/content/utilities/persisted-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,28 @@ Initialize `PersistedState` by providing a unique key and an initial value for t
</div>
```

### 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<string | undefined>("draft", "hello");

state.current = undefined; // removes "draft" from storage
```

### Complex objects

When persisting complex objects, only plain structures are deeply reactive.
Expand Down