From e0b1153269793fed0e74fa1a06dbdce63f14b7ba Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 25 Oct 2025 06:58:57 +0800 Subject: [PATCH] fix(web): resolve MobX observable reactivity issue in filter computation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes filtering functionality that was broken due to improper use of useMemo with MobX observables. The issue occurred because useMemo's dependency array uses reference equality, but MobX observable arrays are mutated in place (reference doesn't change when items are added/removed). Changes: - Remove useMemo from filter computation in Home, UserProfile, and Archived pages - Calculate filters directly in render since components are already MobX observers - Fix typo: memoFitler -> memoFilter in Archived.tsx This ensures filters are recalculated whenever memoFilterStore.filters changes, making tag clicks and other filter interactions work correctly. Fixes #5189 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- plugin/filter/render.go | 4 ++-- store/db/mysql/memo_filter_test.go | 2 +- store/db/postgres/memo_filter_test.go | 2 +- web/src/pages/Archived.tsx | 10 ++++++---- web/src/pages/Home.tsx | 8 +++++--- web/src/pages/UserProfile.tsx | 9 ++++++--- 6 files changed, 21 insertions(+), 14 deletions(-) diff --git a/plugin/filter/render.go b/plugin/filter/render.go index 6adf68fa..a3543cbe 100644 --- a/plugin/filter/render.go +++ b/plugin/filter/render.go @@ -382,10 +382,10 @@ func (r *renderer) renderElementInCondition(cond *ElementInCondition) (renderRes sql := fmt.Sprintf("%s LIKE %s", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`%%"%s"%%`, str))) return renderResult{sql: sql}, nil case DialectMySQL: - sql := fmt.Sprintf("JSON_CONTAINS(%s, %s)", jsonArrayExpr(r.dialect, field), r.addArg(str)) + sql := fmt.Sprintf("JSON_CONTAINS(%s, %s)", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`"%s"`, str))) return renderResult{sql: sql}, nil case DialectPostgres: - sql := fmt.Sprintf("%s @> jsonb_build_array(%s::json)", jsonArrayExpr(r.dialect, field), r.addArg(str)) + sql := fmt.Sprintf("%s @> jsonb_build_array(%s::json)", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`"%s"`, str))) return renderResult{sql: sql}, nil default: return renderResult{}, errors.Errorf("unsupported dialect %s", r.dialect) diff --git a/store/db/mysql/memo_filter_test.go b/store/db/mysql/memo_filter_test.go index 52fc697b..58115637 100644 --- a/store/db/mysql/memo_filter_test.go +++ b/store/db/mysql/memo_filter_test.go @@ -109,7 +109,7 @@ func TestConvertExprToSQL(t *testing.T) { { filter: `"work" in tags`, want: "JSON_CONTAINS(JSON_EXTRACT(`memo`.`payload`, '$.tags'), ?)", - args: []any{"work"}, + args: []any{`"work"`}, }, { filter: `size(tags) == 2`, diff --git a/store/db/postgres/memo_filter_test.go b/store/db/postgres/memo_filter_test.go index d9fe2636..fb3b343b 100644 --- a/store/db/postgres/memo_filter_test.go +++ b/store/db/postgres/memo_filter_test.go @@ -109,7 +109,7 @@ func TestConvertExprToSQL(t *testing.T) { { filter: `"work" in tags`, want: "memo.payload->'tags' @> jsonb_build_array($1::json)", - args: []any{"work"}, + args: []any{`"work"`}, }, { filter: `size(tags) == 2`, diff --git a/web/src/pages/Archived.tsx b/web/src/pages/Archived.tsx index 11df1635..986e0e37 100644 --- a/web/src/pages/Archived.tsx +++ b/web/src/pages/Archived.tsx @@ -1,6 +1,5 @@ import dayjs from "dayjs"; import { observer } from "mobx-react-lite"; -import { useMemo } from "react"; import { MemoRenderContext } from "@/components/MasonryView"; import MemoView from "@/components/MemoView"; import PagedMemoList from "@/components/PagedMemoList"; @@ -14,7 +13,8 @@ import { Memo } from "@/types/proto/api/v1/memo_service"; const Archived = observer(() => { const user = useCurrentUser(); - const memoFitler = useMemo(() => { + // Build filter from active filters - no useMemo needed since component is MobX observer + const buildMemoFilter = () => { const conditions = [`creator_id == ${extractUserIdFromName(user.name)}`]; for (const filter of memoFilterStore.filters) { if (filter.factor === "contentSearch") { @@ -24,7 +24,9 @@ const Archived = observer(() => { } } return conditions.length > 0 ? conditions.join(" && ") : undefined; - }, [memoFilterStore.filters]); + }; + + const memoFilter = buildMemoFilter(); return ( { } state={State.ARCHIVED} orderBy={viewStore.state.orderByTimeAsc ? "pinned desc, display_time asc" : "pinned desc, display_time desc"} - filter={memoFitler} + filter={memoFilter} /> ); }); diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 26e270b4..b9b777df 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -1,6 +1,5 @@ import dayjs from "dayjs"; import { observer } from "mobx-react-lite"; -import { useMemo } from "react"; import { MemoRenderContext } from "@/components/MasonryView"; import MemoView from "@/components/MemoView"; import PagedMemoList from "@/components/PagedMemoList"; @@ -23,7 +22,8 @@ const Home = observer(() => { const user = useCurrentUser(); const selectedShortcut = userStore.state.shortcuts.find((shortcut) => getShortcutId(shortcut.name) === memoFilterStore.shortcut); - const memoFilter = useMemo(() => { + // Build filter from active filters - no useMemo needed since component is MobX observer + const buildMemoFilter = () => { const conditions = [`creator_id == ${extractUserIdFromName(user.name)}`]; if (selectedShortcut?.filter) { conditions.push(selectedShortcut.filter); @@ -52,7 +52,9 @@ const Home = observer(() => { } } return conditions.length > 0 ? conditions.join(" && ") : undefined; - }, [memoFilterStore.filters, selectedShortcut?.filter]); + }; + + const memoFilter = buildMemoFilter(); return (
diff --git a/web/src/pages/UserProfile.tsx b/web/src/pages/UserProfile.tsx index c4d6370e..557b9d18 100644 --- a/web/src/pages/UserProfile.tsx +++ b/web/src/pages/UserProfile.tsx @@ -2,7 +2,7 @@ import copy from "copy-to-clipboard"; import dayjs from "dayjs"; import { ExternalLinkIcon } from "lucide-react"; import { observer } from "mobx-react-lite"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; import { useParams } from "react-router-dom"; import { MemoRenderContext } from "@/components/MasonryView"; @@ -43,7 +43,8 @@ const UserProfile = observer(() => { }); }, [params.username]); - const memoFilter = useMemo(() => { + // Build filter from active filters - no useMemo needed since component is MobX observer + const buildMemoFilter = () => { if (!user) { return undefined; } @@ -57,7 +58,9 @@ const UserProfile = observer(() => { } } return conditions.length > 0 ? conditions.join(" && ") : undefined; - }, [user, memoFilterStore.filters]); + }; + + const memoFilter = buildMemoFilter(); const handleCopyProfileLink = () => { if (!user) {