Gallery's filter (search, sort, folder, favorites-only) doesn't survive a page refresh — user with a long search query and a specific folder has to re-enter the filter every time #232

Closed
opened 2026-10-04 02:16:26 +00:00 by Mark · 1 comment
Owner

Finding

src/pages/Gallery.tsx's filter state is local React useState:

  • searchQuery: string (line 38)
  • sortMode: SortMode (line 39)
  • favoritesOnly: boolean (line 40)
  • activeFolder: string (line 37)

None of these are persisted to localStorage or the URL. A user who:

  • Searches for "logo"
  • Selects the "Work" folder
  • Picks "Most stitches" sort
  • Toggles favorites-only

…refreshes the page. The Gallery reloads with searchQuery: '', sortMode: 'newest', etc. All filter state is lost.

grep -n 'localStorage' src/pages/Gallery.tsx returns 0 hits. grep -n 'useSearchParams' src/pages/Gallery.tsx returns 0.

This is a common UX papercut: filters that disappear on refresh are frustrating. Most modern apps persist filter state in either:

  • The URL (deep-linkable, shareable)
  • localStorage (per-user, silent persistence)
  • Both (URL is canonical, localStorage is the per-user override)

Where

  • Gallery state: src/pages/Gallery.tsx:37-40
  • No persistence: grep -n 'localStorage' src/pages/Gallery.tsx returns 0
  • The design list itself: localStorage.getItem('designs') (per #148) — the designs persist; the filter doesn't.

Suggested fix shape

Two options, in order of preference:

  1. URL persistence (deep-linkable, shareable, no extra code):
import { useSearchParams } from 'react-router-dom';

const [searchParams, setSearchParams] = useSearchParams();
const searchQuery = searchParams.get('q') ?? '';
const sortMode = (searchParams.get('sort') as SortMode) ?? 'newest';
const favoritesOnly = searchParams.get('fav') === '1';
const activeFolder = searchParams.get('folder') ?? 'all';

const updateFilter = (key: string, value: string) => {
  const next = new URLSearchParams(searchParams);
  if (value) next.set(key, value);
  else next.delete(key);
  setSearchParams(next);
};

The user can now share the URL "logo" + "Work folder" + "Most stitches" with a teammate.

  1. localStorage (simpler, no shareable URL):
const [searchQuery, setSearchQuery] = useState(() => localStorage.getItem('gallery.search') ?? '');
useEffect(() => {
  localStorage.setItem('gallery.search', searchQuery);
}, [searchQuery]);

Severity

Low. The user can re-enter the filter. Power users with complex filter stacks notice.

Discovered by

Gap analysis (2026-10-02) of gallery state persistence.

  • The Workspace's filter state (e.g., uploaded image) persists via localStorage (LOAD_DESIGN_STORAGE_KEY). The Gallery should do the same.
  • The Editor's loadedDesignId is read from sessionStorage — different model, different scope.
  • Most apps use URL params for filters; this is the standard pattern.
## Finding `src/pages/Gallery.tsx`'s filter state is local React `useState`: - `searchQuery: string` (line 38) - `sortMode: SortMode` (line 39) - `favoritesOnly: boolean` (line 40) - `activeFolder: string` (line 37) None of these are persisted to `localStorage` or the URL. A user who: - Searches for "logo" - Selects the "Work" folder - Picks "Most stitches" sort - Toggles favorites-only …refreshes the page. The Gallery reloads with `searchQuery: ''`, `sortMode: 'newest'`, etc. All filter state is lost. `grep -n 'localStorage' src/pages/Gallery.tsx` returns 0 hits. `grep -n 'useSearchParams' src/pages/Gallery.tsx` returns 0. This is a common UX papercut: filters that disappear on refresh are frustrating. Most modern apps persist filter state in either: - The URL (deep-linkable, shareable) - localStorage (per-user, silent persistence) - Both (URL is canonical, localStorage is the per-user override) ## Where - Gallery state: `src/pages/Gallery.tsx:37-40` - No persistence: `grep -n 'localStorage' src/pages/Gallery.tsx` returns 0 - The design list itself: `localStorage.getItem('designs')` (per #148) — the designs persist; the filter doesn't. ## Suggested fix shape Two options, in order of preference: 1. **URL persistence** (deep-linkable, shareable, no extra code): ```ts import { useSearchParams } from 'react-router-dom'; const [searchParams, setSearchParams] = useSearchParams(); const searchQuery = searchParams.get('q') ?? ''; const sortMode = (searchParams.get('sort') as SortMode) ?? 'newest'; const favoritesOnly = searchParams.get('fav') === '1'; const activeFolder = searchParams.get('folder') ?? 'all'; const updateFilter = (key: string, value: string) => { const next = new URLSearchParams(searchParams); if (value) next.set(key, value); else next.delete(key); setSearchParams(next); }; ``` The user can now share the URL "logo" + "Work folder" + "Most stitches" with a teammate. 2. **localStorage** (simpler, no shareable URL): ```ts const [searchQuery, setSearchQuery] = useState(() => localStorage.getItem('gallery.search') ?? ''); useEffect(() => { localStorage.setItem('gallery.search', searchQuery); }, [searchQuery]); ``` ## Severity Low. The user can re-enter the filter. Power users with complex filter stacks notice. ## Discovered by Gap analysis (2026-10-02) of gallery state persistence. ## Related - The Workspace's filter state (e.g., uploaded image) persists via localStorage (`LOAD_DESIGN_STORAGE_KEY`). The Gallery should do the same. - The Editor's `loadedDesignId` is read from sessionStorage — different model, different scope. - Most apps use URL params for filters; this is the standard pattern.
Author
Owner

Shipped in commit b3ab1b0.

Files:

  • src/components/gallery/persistedFilters.ts (new): helpers and per-slot validation
  • src/pages/Gallery.tsx (modified): read on mount, write on change
  • src/pages/Gallery.filterPersistence.test.tsx (new): 6 tests passing

Chose localStorage over URL persistence to match the established pattern.

Test run: 1162 passing (1156 -> 1162, +6 new). Typecheck clean.

Shipped in commit b3ab1b0. Files: - src/components/gallery/persistedFilters.ts (new): helpers and per-slot validation - src/pages/Gallery.tsx (modified): read on mount, write on change - src/pages/Gallery.filterPersistence.test.tsx (new): 6 tests passing Chose localStorage over URL persistence to match the established pattern. Test run: 1162 passing (1156 -> 1162, +6 new). Typecheck clean.
Mark closed this issue 2026-10-04 07:01:06 +00:00
Sign in to join this conversation.
No description provided.