Commit e0b11532 authored by Steven's avatar Steven

fix(web): resolve MobX observable reactivity issue in filter computation

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: 's avatarClaude <noreply@anthropic.com>
parent 46ce0bc6
...@@ -382,10 +382,10 @@ func (r *renderer) renderElementInCondition(cond *ElementInCondition) (renderRes ...@@ -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))) sql := fmt.Sprintf("%s LIKE %s", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`%%"%s"%%`, str)))
return renderResult{sql: sql}, nil return renderResult{sql: sql}, nil
case DialectMySQL: 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 return renderResult{sql: sql}, nil
case DialectPostgres: 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 return renderResult{sql: sql}, nil
default: default:
return renderResult{}, errors.Errorf("unsupported dialect %s", r.dialect) return renderResult{}, errors.Errorf("unsupported dialect %s", r.dialect)
......
...@@ -109,7 +109,7 @@ func TestConvertExprToSQL(t *testing.T) { ...@@ -109,7 +109,7 @@ func TestConvertExprToSQL(t *testing.T) {
{ {
filter: `"work" in tags`, filter: `"work" in tags`,
want: "JSON_CONTAINS(JSON_EXTRACT(`memo`.`payload`, '$.tags'), ?)", want: "JSON_CONTAINS(JSON_EXTRACT(`memo`.`payload`, '$.tags'), ?)",
args: []any{"work"}, args: []any{`"work"`},
}, },
{ {
filter: `size(tags) == 2`, filter: `size(tags) == 2`,
......
...@@ -109,7 +109,7 @@ func TestConvertExprToSQL(t *testing.T) { ...@@ -109,7 +109,7 @@ func TestConvertExprToSQL(t *testing.T) {
{ {
filter: `"work" in tags`, filter: `"work" in tags`,
want: "memo.payload->'tags' @> jsonb_build_array($1::json)", want: "memo.payload->'tags' @> jsonb_build_array($1::json)",
args: []any{"work"}, args: []any{`"work"`},
}, },
{ {
filter: `size(tags) == 2`, filter: `size(tags) == 2`,
......
import dayjs from "dayjs"; import dayjs from "dayjs";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useMemo } from "react";
import { MemoRenderContext } from "@/components/MasonryView"; import { MemoRenderContext } from "@/components/MasonryView";
import MemoView from "@/components/MemoView"; import MemoView from "@/components/MemoView";
import PagedMemoList from "@/components/PagedMemoList"; import PagedMemoList from "@/components/PagedMemoList";
...@@ -14,7 +13,8 @@ import { Memo } from "@/types/proto/api/v1/memo_service"; ...@@ -14,7 +13,8 @@ import { Memo } from "@/types/proto/api/v1/memo_service";
const Archived = observer(() => { const Archived = observer(() => {
const user = useCurrentUser(); 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)}`]; const conditions = [`creator_id == ${extractUserIdFromName(user.name)}`];
for (const filter of memoFilterStore.filters) { for (const filter of memoFilterStore.filters) {
if (filter.factor === "contentSearch") { if (filter.factor === "contentSearch") {
...@@ -24,7 +24,9 @@ const Archived = observer(() => { ...@@ -24,7 +24,9 @@ const Archived = observer(() => {
} }
} }
return conditions.length > 0 ? conditions.join(" && ") : undefined; return conditions.length > 0 ? conditions.join(" && ") : undefined;
}, [memoFilterStore.filters]); };
const memoFilter = buildMemoFilter();
return ( return (
<PagedMemoList <PagedMemoList
...@@ -47,7 +49,7 @@ const Archived = observer(() => { ...@@ -47,7 +49,7 @@ const Archived = observer(() => {
} }
state={State.ARCHIVED} state={State.ARCHIVED}
orderBy={viewStore.state.orderByTimeAsc ? "pinned desc, display_time asc" : "pinned desc, display_time desc"} orderBy={viewStore.state.orderByTimeAsc ? "pinned desc, display_time asc" : "pinned desc, display_time desc"}
filter={memoFitler} filter={memoFilter}
/> />
); );
}); });
......
import dayjs from "dayjs"; import dayjs from "dayjs";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useMemo } from "react";
import { MemoRenderContext } from "@/components/MasonryView"; import { MemoRenderContext } from "@/components/MasonryView";
import MemoView from "@/components/MemoView"; import MemoView from "@/components/MemoView";
import PagedMemoList from "@/components/PagedMemoList"; import PagedMemoList from "@/components/PagedMemoList";
...@@ -23,7 +22,8 @@ const Home = observer(() => { ...@@ -23,7 +22,8 @@ const Home = observer(() => {
const user = useCurrentUser(); const user = useCurrentUser();
const selectedShortcut = userStore.state.shortcuts.find((shortcut) => getShortcutId(shortcut.name) === memoFilterStore.shortcut); 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)}`]; const conditions = [`creator_id == ${extractUserIdFromName(user.name)}`];
if (selectedShortcut?.filter) { if (selectedShortcut?.filter) {
conditions.push(selectedShortcut.filter); conditions.push(selectedShortcut.filter);
...@@ -52,7 +52,9 @@ const Home = observer(() => { ...@@ -52,7 +52,9 @@ const Home = observer(() => {
} }
} }
return conditions.length > 0 ? conditions.join(" && ") : undefined; return conditions.length > 0 ? conditions.join(" && ") : undefined;
}, [memoFilterStore.filters, selectedShortcut?.filter]); };
const memoFilter = buildMemoFilter();
return ( return (
<div className="w-full min-h-full bg-background text-foreground"> <div className="w-full min-h-full bg-background text-foreground">
......
...@@ -2,7 +2,7 @@ import copy from "copy-to-clipboard"; ...@@ -2,7 +2,7 @@ import copy from "copy-to-clipboard";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ExternalLinkIcon } from "lucide-react"; import { ExternalLinkIcon } from "lucide-react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { MemoRenderContext } from "@/components/MasonryView"; import { MemoRenderContext } from "@/components/MasonryView";
...@@ -43,7 +43,8 @@ const UserProfile = observer(() => { ...@@ -43,7 +43,8 @@ const UserProfile = observer(() => {
}); });
}, [params.username]); }, [params.username]);
const memoFilter = useMemo(() => { // Build filter from active filters - no useMemo needed since component is MobX observer
const buildMemoFilter = () => {
if (!user) { if (!user) {
return undefined; return undefined;
} }
...@@ -57,7 +58,9 @@ const UserProfile = observer(() => { ...@@ -57,7 +58,9 @@ const UserProfile = observer(() => {
} }
} }
return conditions.length > 0 ? conditions.join(" && ") : undefined; return conditions.length > 0 ? conditions.join(" && ") : undefined;
}, [user, memoFilterStore.filters]); };
const memoFilter = buildMemoFilter();
const handleCopyProfileLink = () => { const handleCopyProfileLink = () => {
if (!user) { if (!user) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment