fix(playlists): a list reopened from the cache keeps its rows

Leaving the Playlists page and coming back painted the selected list empty ("0 videos") while
the rail counted its rows. The page re-seeds its undoable order from the cached list in an
effect; StrictMode runs that effect twice on the remount, so a reset was followed in the same
tick by a rebase. Since E32 S3 a reset is state only and the rebase read the mirror ref, which
still held the empty initial history, and wrote it back over the reset.

rebase is now a state updater, so it builds on whatever an earlier call in the tick installed.
Both tests seen RED (hook: [] for [10, 20]; page: no rows), and the bug reproduced in the
browser on the old hook.
This commit is contained in:
2026-10-04 02:48:10 +02:00
parent 98ec1abf25
commit 3741d1a46d
3 changed files with 60 additions and 14 deletions
+29
View File
@@ -0,0 +1,29 @@
// @vitest-environment jsdom
//
// The undo container chains calls made in one tick: a `reset` not yet committed is what the next
// call builds on. A `rebase` read the mirror ref, which only follows a commit, so it rebased the
// value from BEFORE the reset and wrote that back — the playlist page, remounted under StrictMode
// over a cached list, re-ran its seeding effect (reset, then rebase) and painted an empty list.
import { describe, expect, it } from "vitest";
import { act, renderHook } from "@testing-library/react";
import { useUndoable } from "./useUndoable";
describe("useUndoable", () => {
it("rebases the value a reset in the same tick installed, not the one before it", () => {
const { result } = renderHook(() => useUndoable<number[]>([]));
act(() => {
result.current.reset([1, 2]);
result.current.rebase((xs) => xs.map((x) => x * 10));
});
expect(result.current.value).toEqual([10, 20]);
});
it("keeps the undo history through a rebase", () => {
const { result } = renderHook(() => useUndoable<number[]>([1]));
act(() => result.current.set([1, 2]));
act(() => result.current.rebase((xs) => xs.map((x) => x + 1)));
expect(result.current.value).toEqual([2, 3]);
act(() => result.current.undo());
expect(result.current.value).toEqual([2]);
});
});