Make doubly sure that useLocalStorage reacts to key changes

I think my previous commit technically made sure that the value would converge to the right thing eventually, but it could become temporarily out of sync with the key passed to the hook, at least. The test demonstrates how.

We haven't yet triggered this failure mode in practice, I think; this is more of a theoretical correctness thing.
This commit is contained in:
Robin
2025-05-28 12:32:45 -04:00
parent 6258bcec54
commit ccf168cadd
2 changed files with 31 additions and 4 deletions

View File

@@ -7,7 +7,8 @@ Please see LICENSE in the repository root for full details.
import { test } from "vitest";
import { render, screen } from "@testing-library/react";
import { type FC, useEffect } from "react";
import { type FC, useEffect, useState } from "react";
import userEvent from "@testing-library/user-event";
import { setLocalStorageItem, useLocalStorage } from "./useLocalStorage";
@@ -21,3 +22,27 @@ test("useLocalStorage reacts to changes made by an effect mounted on the same re
render(<Test />);
screen.getByText("Hello!");
});
test("useLocalStorage reacts to key changes", async () => {
localStorage.clear();
localStorage.setItem("value-1", "1");
localStorage.setItem("value-2", "2");
const Test: FC = () => {
const [key, setKey] = useState("value-1");
const [value] = useLocalStorage(key);
if (key !== `value-${value}`) throw new Error("Value is out of sync");
return (
<>
<button onClick={() => setKey("value-2")}>Switch keys</button>
<div>Value is: {value}</div>
</>
);
};
const user = userEvent.setup();
render(<Test />);
screen.getByText("Value is: 1");
await user.click(screen.getByRole("button", { name: "Switch keys" }));
screen.getByText("Value is: 2");
});

View File

@@ -6,9 +6,10 @@ Please see LICENSE in the repository root for full details.
*/
import EventEmitter from "events";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect } from "react";
import { useLatest } from "./useLatest";
import { useReactiveState } from "./useReactiveState";
type LocalStorageItem = ReturnType<typeof localStorage.getItem>;
@@ -19,8 +20,9 @@ export const localStorageBus = new EventEmitter();
export const useLocalStorage = (
key: string,
): [LocalStorageItem, (value: string) => void] => {
const [value, setValue] = useState<LocalStorageItem>(() =>
localStorage.getItem(key),
const [value, setValue] = useReactiveState<LocalStorageItem>(
() => localStorage.getItem(key),
[key],
);
const latestValue = useLatest(value);