Skip to content

Commit

Permalink
add tests for useLocalStorage
Browse files Browse the repository at this point in the history
  • Loading branch information
konstantin-lukas committed Sep 7, 2024
1 parent bdc21cc commit 7d29140
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 4 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Anzol is built alongside automated tests to ensure quality.
- <b>useIntersectionObserverArray:</b> Like useIntersectionObserver but for multiple elements.
- <b>useEvent:</b> Provides a wrapper around the EventListener API. Use the return value to define the event target.
- <b>useLazyLoad:</b> Provides a simple API for fetching data from a resource in batches.
- <b>useLocalStorage:</b> Provides access to local storage, with the additional option to update all usages of this hook
when local storage changes.

## Installation
Anzol is available on the NPM registry. To install it, just run:
Expand All @@ -47,8 +49,6 @@ These features are not yet implement but we plan to do so in the foreseeable fut
suggestions.
- <b>useClickOutside:</b> Takes a reference to an HTML element and a callback function, and calls that function when the
user clicks anywhere outside the given element.
- <b>useLocalStorage:</b> Provides access to local storage, with the additional option to update all usages of this hook
when local storage changes.
- <b>usePreferredScheme:</b> Listens for changes in the user's preferred scheme and returns it.
- <b>useDarkMode:</b> Similar to usePreferredScheme but allows setting the user scheme manually and automatically
updates it when the preferred scheme changes. Uses local storage to save the chosen scheme across reloads.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "anzol",
"version": "2.3.0",
"version": "2.4.0",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
Expand Down
9 changes: 8 additions & 1 deletion src/hooks/useLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Dispatch, SetStateAction } from "react";
import { useEffect, useState, useMemo } from "react";
import { useFirstRender } from "../index";

export interface LocalStorageOptions {
/** If there is no value for the specified {@link key}, it automatically set this value on mount. */
Expand Down Expand Up @@ -37,7 +38,13 @@ function useLocalStorage(key: string, {
listenForChanges = false,
}: LocalStorageOptions = {}): [string | null, Dispatch<SetStateAction<string | null>>] {

const [value, setValue] = useState(localStorage.getItem(key) || initialValue || null);
const [value, setValue] = useState(() => {
const storedValue = localStorage.getItem(key);
if (storedValue) return storedValue;
const init = initialValue || null;
if (init) localStorage.setItem(key, init);
return init;
});
const [blockUpdates, setBlockUpdates] = useState(true);
const id = useMemo(() => instanceCount++, []);

Expand Down
112 changes: 112 additions & 0 deletions tests/useLocalStorage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { act, renderHook } from "@testing-library/react";
import { useLocalStorage, type LocalStorageOptions } from "../src";

async function renderUseLocalStorage({
initialKey,
initialOptions,
}: {
initialKey: string,
initialOptions?: LocalStorageOptions
}) {
const {
result,
rerender,
} = await act(async () => {
return renderHook(({ key, options }) => useLocalStorage(key, options), {
initialProps: {
key: initialKey,
options: initialOptions ?? {},
},
});
});
return { rerender, result };
}

test("should set a default value when local storage item doesn't exist", async () => {
const { result } = await renderUseLocalStorage({ initialKey: "animal", initialOptions: { initialValue: "Bear" }});
expect(result.current[0]).toBe("Bear");
expect(window.localStorage.getItem("animal")).toBe("Bear");
});

test("should not set a default value when local storage item does exist", async () => {
const { result: result1 } = await renderUseLocalStorage({ initialKey: "animal", initialOptions: { initialValue: "Bear" }});
expect(result1.current[0]).toBe("Bear");
expect(window.localStorage.getItem("animal")).toBe("Bear");
const { result: result2 } = await renderUseLocalStorage({ initialKey: "animal", initialOptions: { initialValue: "Duck" }});
expect(result2.current[0]).toBe("Bear");
expect(window.localStorage.getItem("animal")).toBe("Bear");
});

test("should not update other instances by default", async () => {
const { result: result1 } = await renderUseLocalStorage({ initialKey: "animal", initialOptions: { initialValue: "Bear" }});
expect(result1.current[0]).toBe("Bear");
const { result: result2 } = await renderUseLocalStorage({ initialKey: "animal", initialOptions: { initialValue: "Duck" }});
expect(result2.current[0]).toBe("Bear");

act(() => result1.current[1]("Giraffe"));
expect(result1.current[0]).toBe("Giraffe");
expect(result2.current[0]).toBe("Bear");
});

test("should update other instances only if configured to listen and emit", async () => {
const {
result: result1,
} = await renderUseLocalStorage({ initialKey: "animal", initialOptions: { propagateChanges: true }});
const {
rerender,
result: result2,
} = await renderUseLocalStorage({ initialKey: "animal", initialOptions: { listenForChanges: true }});

act(() => result1.current[1]("Giraffe"));
expect(result1.current[0]).toBe("Giraffe");
expect(result2.current[0]).toBe("Giraffe");

rerender({ key: "animal", options: { listenForChanges: false }});
act(() => result1.current[1]("Chimpanzee"));
expect(result1.current[0]).toBe("Chimpanzee");
expect(result2.current[0]).toBe("Giraffe");
});

test("should allow switching between keys", async () => {
const {
result: result1,
} = await renderUseLocalStorage({ initialKey: "animal", initialOptions: { propagateChanges: true, listenForChanges: true }});
const {
rerender,
result: result2,
} = await renderUseLocalStorage({ initialKey: "animal", initialOptions: { propagateChanges: true, listenForChanges: true }});

act(() => result1.current[1]("Giraffe"));
expect(result1.current[0]).toBe("Giraffe");
expect(result2.current[0]).toBe("Giraffe");

rerender({ key: "fruit", options: { listenForChanges: true }});
act(() => result2.current[1]("Apples"));
expect(result1.current[0]).toBe("Giraffe");
expect(result2.current[0]).toBe("Apples");
});

test("should not emit more events than necessarya", async () => {
Object.defineProperty(window, "localStorage", {
value: {
setItem: jest.fn(),
getItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
},
writable: true,
});
const setItemSpy = jest.spyOn(window.localStorage, "setItem");

const {
result: result1,
} = await renderUseLocalStorage({ initialKey: "animal", initialOptions: { propagateChanges: true, listenForChanges: true }});
const {
result: result2,
} = await renderUseLocalStorage({ initialKey: "animal", initialOptions: { propagateChanges: true, listenForChanges: true }});

expect(setItemSpy).toHaveBeenCalledTimes(0);
act(() => result1.current[1]("Eagle"));
act(() => result2.current[1]("Unicorn"));
expect(setItemSpy).toHaveBeenCalledTimes(2);
});

0 comments on commit 7d29140

Please sign in to comment.