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 6adf68fad..a3543cbe6 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 52fc697b9..58115637b 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 d9fe2636e..fb3b343b6 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 11df16355..986e0e371 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 26e270b40..b9b777df6 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 c4d6370e8..557b9d18f 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) {