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: Claude <noreply@anthropic.com>
release/0.25.2
Steven 1 week ago
parent 46ce0bc62e
commit e0b1153269

@ -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)

@ -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`,

@ -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`,

@ -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 (
<PagedMemoList
@ -47,7 +49,7 @@ const Archived = observer(() => {
}
state={State.ARCHIVED}
orderBy={viewStore.state.orderByTimeAsc ? "pinned desc, display_time asc" : "pinned desc, display_time desc"}
filter={memoFitler}
filter={memoFilter}
/>
);
});

@ -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 (
<div className="w-full min-h-full bg-background text-foreground">

@ -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) {

Loading…
Cancel
Save