Write useLocalStorage(key, initialValue) and export it from useLocalStorage.js. It works like useState, returning [value, setValue], but the value is saved in localStorage under key, so the tea shop's cart is still there after a reload.
Read localStorage once, when the component first renders, and parse what's saved with JSON.parse. If nothing is saved, start with initialValue. If what's saved isn't valid JSON (someone edited it by hand), don't crash: start with initialValue.
setValue(next) sets the value and saves JSON.stringify(next) under key. Like a state setter, it also takes an updater, setValue((current) => next), and two updates in the same event both apply.
Other tabs share the same storage. When another tab changes it, the browser fires a storage event on window with the key that changed and its newValue: a string, or null when the item was removed. When the event's key is yours, take the new value. When the item was removed, or the event's key is null (the other tab cleared all of storage), go back to initialValue. Ignore events for other keys, and stop listening when the component goes away.
You can assume key stays the same for as long as the component is on the page.
function Cart() {
const [teas, setTeas] = useLocalStorage("cart", []);
return (
<div>
<p>{teas.length} in the cart</p>
<button onClick={() => setTeas((current) => [...current, "Sencha"])}>Add Sencha</button>
</div>
);
}
The Preview shows Demo.jsx, a cart that uses your hook. Reload the Preview and the cart is still there.
const { result } = renderHook(() => useLocalStorage("cart", []));
expect(result.current[0]).toEqual([]);
localStorage.setItem("cart", '["Sencha","Oolong"]');
const { result } = renderHook(() => useLocalStorage("cart", []));
expect(result.current[0]).toEqual(["Sencha", "Oolong"]);
const { result } = renderHook(() => useLocalStorage("cart", []));
act(() => result.current[1](["Assam"]));
expect(result.current[0]).toEqual(["Assam"]);
expect(localStorage.getItem("cart")).toBe('["Assam"]');
const { result } = renderHook(() => useLocalStorage("cups", 0));
const setCups = result.current[1];
act(() => {
setCups((cups) => cups + 1);
setCups((cups) => cups + 1);
});
expect(result.current[0]).toBe(2);
expect(localStorage.getItem("cups")).toBe("2");
localStorage.setItem("cart", "{oops");
const { result } = renderHook(() => useLocalStorage("cart", []));
expect(result.current[0]).toEqual([]);
const read = localStorage.getItem;
let reads = 0;
localStorage.getItem = (key) => {
if (key === "cart") reads++;
return read(key);
};
const { result, rerender } = renderHook(() => useLocalStorage("cart", []));
rerender();
rerender();
act(() => result.current[1](["Sencha"]));
expect(reads).toBe(1);
function Cart() {
const [teas, setTeas] = useLocalStorage("cart", []);
return (
<div>
<p>{teas.length} in the cart</p>
<button onClick={() => setTeas((current) => [...current, "Sencha"])}>Add Sencha</button>
</div>
);
}
const first = render(<Cart />);
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
first.unmount();
render(<Cart />);
expect(screen.getByText("2 in the cart")).toBeInTheDocument();
const { result } = renderHook(() => useLocalStorage("cart", []));
act(() => {
window.dispatchEvent(new StorageEvent("storage", { key: "cart", newValue: '["Gyokuro"]' }));
});
expect(result.current[0]).toEqual(["Gyokuro"]);
act(() => {
window.dispatchEvent(new StorageEvent("storage", { key: "theme", newValue: '"dark"' }));
});
expect(result.current[0]).toEqual(["Gyokuro"]);
localStorage.setItem("cart", '["Sencha"]');
const { result } = renderHook(() => useLocalStorage("cart", []));
act(() => {
window.dispatchEvent(new StorageEvent("storage", { key: "cart", newValue: null }));
});
expect(result.current[0]).toEqual([]);
act(() => {
window.dispatchEvent(new StorageEvent("storage", { key: "cart", newValue: '["Assam"]' }));
});
act(() => {
window.dispatchEvent(new StorageEvent("storage", { key: null, newValue: null }));
});
expect(result.current[0]).toEqual([]);
const listening = new Set();
const add = window.addEventListener;
const remove = window.removeEventListener;
window.addEventListener = function (type, listener, options) {
if (type === "storage") listening.add(listener);
return add.call(this, type, listener, options);
};
window.removeEventListener = function (type, listener, options) {
if (type === "storage") listening.delete(listener);
return remove.call(this, type, listener, options);
};
const { unmount } = renderHook(() => useLocalStorage("cart", []));
expect(listening.size).toBeGreaterThan(0);
unmount();
expect(listening.size).toBe(0);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with useLocalStorage. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.