refactor(web): leave memoization to the React Compiler where it compiles

Removes useCallback, useMemo and memo from the 27 files the compiler compiles without bailing. Files it bails on (destructuring defaults, try/finally, refs read during render) keep their manual memoization.
This commit is contained in:
Amruth Pillai
2026-09-29 10:50:26 +02:00
parent eae7de3fb0
commit 218aad51a8
23 changed files with 101 additions and 169 deletions
@@ -3,7 +3,6 @@ import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { hsvaToRgbaString, rgbaStringToHsva } from "@uiw/color-convert";
import ReactColorColorful from "@uiw/react-color-colorful";
import { useMemo } from "react";
import { Popover, PopoverContent, PopoverTrigger } from "@reactive-resume/ui/components/popover";
import { Separator } from "@reactive-resume/ui/components/separator";
import { cn } from "@reactive-resume/utils/style";
@@ -53,7 +52,7 @@ export function ColorPicker({
onChange,
});
const color = useMemo(() => rgbaStringToHsva(currentValue), [currentValue]);
const color = rgbaStringToHsva(currentValue);
function onColorChange(color: ColorResult) {
const rgbaString = hsvaToRgbaString(color.hsva);
@@ -3,7 +3,7 @@ import type { CellComponentProps } from "react-window";
import { t } from "@lingui/core/macro";
import { ProhibitIcon } from "@phosphor-icons/react";
import Fuse from "fuse.js";
import { useMemo, useState } from "react";
import { useState } from "react";
import { Grid } from "react-window";
import { icons } from "@reactive-resume/schema/icons";
import { Button } from "@reactive-resume/ui/components/button";
@@ -81,7 +81,7 @@ type IconPickerProps = Omit<React.ComponentProps<typeof Button>, "value" | "onCh
export function IconPicker({ value, onChange, popoverProps, ...props }: IconPickerProps) {
const [search, setSearch] = useState("");
const searchedIcons = useMemo(() => searchIcons(search), [search]);
const searchedIcons = searchIcons(search);
const rowCount = Math.ceil(searchedIcons.length / columnCount);
return (
+9 -16
View File
@@ -1,7 +1,6 @@
import type { Website } from "@reactive-resume/schema/resume/data";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { useCallback } from "react";
import { Icon } from "@reactive-resume/ui/components/icon";
import { Input } from "@reactive-resume/ui/components/input";
import {
@@ -35,22 +34,16 @@ type Props<TValue extends Website = Website> = Omit<React.ComponentProps<"input"
export function URLInput<TValue extends Website>({ value, onChange, hideLabelButton, ...props }: Props<TValue>) {
const prefix = value.url.match(HTTP_PREFIX)?.[0] ?? DEFAULT_PREFIX;
const handleUrlChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
onChange({
...value,
url: ensurePrefix(e.target.value, prefix),
});
},
[onChange, value, prefix],
);
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange({
...value,
url: ensurePrefix(e.target.value, prefix),
});
};
const handleLabelChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
onChange({ ...value, label: e.target.value });
},
[onChange, value],
);
const handleLabelChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange({ ...value, label: e.target.value });
};
const urlValue = stripPrefix(value.url);
+4 -7
View File
@@ -1,7 +1,6 @@
import type z from "zod";
import type { SingleComboboxProps } from "@/components/ui/combobox";
import { t } from "@lingui/core/macro";
import { useMemo } from "react";
import { match } from "ts-pattern";
import { levelDesignSchema } from "@reactive-resume/schema/resume/data";
import { Combobox } from "@/components/ui/combobox";
@@ -23,12 +22,10 @@ const getLevelTypeName = (type: LevelType) => {
};
export function LevelTypeCombobox({ ...props }: LevelTypeComboboxProps) {
const options = useMemo(() => {
return levelDesignSchema.shape.type.options.map((option) => ({
value: option,
label: getLevelTypeName(option),
}));
}, []);
const options = levelDesignSchema.shape.type.options.map((option) => ({
value: option,
label: getLevelTypeName(option),
}));
return <Combobox options={options} {...props} />;
}
@@ -1,5 +1,5 @@
import type { MultiComboboxProps, SingleComboboxProps } from "@/components/ui/combobox";
import { useCallback, useMemo } from "react";
import { useMemo } from "react";
import { fontList, getFont, getFontDisplayName, getFontSearchKeywords, sortFontWeights } from "@reactive-resume/fonts";
import { cn } from "@reactive-resume/utils/style";
import { Combobox } from "@/components/ui/combobox";
@@ -54,18 +54,12 @@ export function FontWeightCombobox({
}));
}, [fontFamily]);
const sortedValue = useMemo(() => (value ? sortFontWeights(value) : value), [value]);
const sortedDefaultValue = useMemo(
() => (defaultValue ? sortFontWeights(defaultValue) : defaultValue),
[defaultValue],
);
const sortedValue = value ? sortFontWeights(value) : value;
const sortedDefaultValue = defaultValue ? sortFontWeights(defaultValue) : defaultValue;
const handleValueChange = useCallback(
(nextValue: string[] | null) => {
onValueChange?.(nextValue ? sortFontWeights(nextValue) : nextValue);
},
[onValueChange],
);
const handleValueChange = (nextValue: string[] | null) => {
onValueChange?.(nextValue ? sortFontWeights(nextValue) : nextValue);
};
return (
<Combobox
@@ -1,6 +1,6 @@
import { t } from "@lingui/core/macro";
import Cookies from "js-cookie";
import { useCallback, useState } from "react";
import { useState } from "react";
import { useTimeout } from "usehooks-ts";
import { toast } from "@reactive-resume/ui/components/toast";
@@ -15,13 +15,13 @@ export function DonationToast() {
// ponytail: inlined from @reactive-resume/ui/hooks/use-cookie — only consumer, one read + one set-with-expiry
const [dismissed, setDismissedState] = useState<string | null>(() => Cookies.get(DISMISSED_COOKIE_NAME) ?? null);
const setDismissed = useCallback((value: string, options?: { expires?: Date }) => {
const setDismissed = (value: string, options?: { expires?: Date }) => {
// Attributes match the former useCookie DEFAULT_COOKIE_ATTRIBUTES; options (expiry) override.
Cookies.set(DISMISSED_COOKIE_NAME, value, { path: "/", secure: true, sameSite: "lax", ...options });
setDismissedState(value);
}, []);
};
const showToast = useCallback(() => {
const showToast = () => {
if (dismissed === "true") return;
toast.add({
@@ -41,7 +41,7 @@ export function DonationToast() {
setDismissed("true", { expires: getDismissedCookieExpiresAt() });
},
});
}, [dismissed, setDismissed]);
};
useTimeout(showToast, SHOW_TOAST_DELAY_MS);
@@ -4,7 +4,7 @@ import type { Application } from "../types";
import { t } from "@lingui/core/macro";
import { useLingui } from "@lingui/react";
import { Trans } from "@lingui/react/macro";
import { useMemo, useState } from "react";
import { useState } from "react";
import { INTERVIEW_KINDS } from "@reactive-resume/schema/applications/data";
import { Button } from "@reactive-resume/ui/components/button";
import { Icon } from "@reactive-resume/ui/components/icon";
@@ -44,11 +44,11 @@ export function ApplicationCalendar({ applications, allApplications, onOpen }: A
});
const [dialog, setDialog] = useState<DialogState>({ open: false });
const interviews = useMemo(() => collectInterviews(applications), [applications]);
const byDay = useMemo(() => groupByDay(interviews), [interviews]);
const upcoming = useMemo(() => upcomingInterviews(interviews), [interviews]);
const upcomingByDay = useMemo(() => [...groupByDay(upcoming).values()], [upcoming]);
const days = useMemo(() => monthGrid(month), [month]);
const interviews = collectInterviews(applications);
const byDay = groupByDay(interviews);
const upcoming = upcomingInterviews(interviews);
const upcomingByDay = [...groupByDay(upcoming).values()];
const days = monthGrid(month);
const today = new Date();
const todayKey = dayKey(today);
@@ -2,7 +2,7 @@ import type { CsvField } from "../csv";
import { t } from "@lingui/core/macro";
import { Plural, Trans } from "@lingui/react/macro";
import { useMutation } from "@tanstack/react-query";
import { useMemo, useRef, useState } from "react";
import { useRef, useState } from "react";
import { Button } from "@reactive-resume/ui/components/button";
import { Icon } from "@reactive-resume/ui/components/icon";
import { Label } from "@reactive-resume/ui/components/label";
@@ -59,13 +59,12 @@ export function ImportApplicationsSheet({ open, onOpenChange }: ImportSheetProps
const [overrides, setOverrides] = useState<Record<number, CsvField | null>>({});
const fileRef = useRef<HTMLInputElement>(null);
const table = useMemo(() => (text.trim() ? parseCsv(text) : null), [text]);
const table = text.trim() ? parseCsv(text) : null;
const headers = table?.[0] ?? [];
const mapping = useMemo(
() => autoMapHeaders(headers).map((field, index) => (index in overrides ? (overrides[index] ?? null) : field)),
[headers, overrides],
const mapping = autoMapHeaders(headers).map((field, index) =>
index in overrides ? (overrides[index] ?? null) : field,
);
const result = useMemo(() => (table ? mapCsvToApplications(table, mapping) : null), [table, mapping]);
const result = table ? mapCsvToApplications(table, mapping) : null;
// The import endpoint takes 500 at a time; the rest is reported rather than refused.
const importable = result ? result.rows.slice(0, MAX_IMPORT) : [];
@@ -3,7 +3,7 @@ import type { Application } from "../types";
import { t } from "@lingui/core/macro";
import { Plural, Trans } from "@lingui/react/macro";
import { useQuery } from "@tanstack/react-query";
import { useMemo, useRef } from "react";
import { useRef } from "react";
import { Button } from "@reactive-resume/ui/components/button";
import { Icon } from "@reactive-resume/ui/components/icon";
import { toast } from "@reactive-resume/ui/components/toast";
@@ -23,26 +23,16 @@ export function ApplicationInsights({ applications }: { applications: Applicatio
const { data: documents } = useQuery(orpc.documents.list.queryOptions({ input: { trashed: false } }));
// A resume is tailored for an application when it was made for it (Copy for a job).
const madeFor = useMemo(
() =>
new Map(
(documents ?? []).flatMap((document) => (document.application ? [[document.id, document.application.id]] : [])),
),
[documents],
const madeFor = new Map(
(documents ?? []).flatMap((document) => (document.application ? [[document.id, document.application.id]] : [])),
);
const outcomes = useMemo(
() =>
computeOutcomes(applications, (application) =>
Boolean(application.resumeId && madeFor.get(application.resumeId) === (application as Application).id),
),
[applications, madeFor],
const outcomes = computeOutcomes(applications, (application) =>
Boolean(application.resumeId && madeFor.get(application.resumeId) === (application as Application).id),
);
// Weekly application velocity — derived from the already-loaded list, closed ones excluded.
const timeline = useMemo(
() =>
computeTimeline(applications.filter((app) => app.status !== "closed").map((app) => new Date(appliedDate(app)))),
[applications],
const timeline = computeTimeline(
applications.filter((app) => app.status !== "closed").map((app) => new Date(appliedDate(app))),
);
const maxWeek = Math.max(1, ...timeline.map((bucket) => bucket.count));
+2 -3
View File
@@ -1,9 +1,8 @@
import { memo } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
/** The assistant's reply as Markdown. Memoized on the text, so finished replies stop re-rendering while one streams. */
export const AssistantMarkdown = memo(function AssistantMarkdown({ text }: { text: string }) {
export const AssistantMarkdown = function AssistantMarkdown({ text }: { text: string }) {
return (
<ReactMarkdown
skipHtml
@@ -41,4 +40,4 @@ export const AssistantMarkdown = memo(function AssistantMarkdown({ text }: { tex
{text}
</ReactMarkdown>
);
});
};
+13 -17
View File
@@ -2,7 +2,6 @@ import type { LetterWords } from "@reactive-resume/resume/cover-letter";
import type { CoverLetter } from "@reactive-resume/schema/cover-letter/data";
import { t } from "@lingui/core/macro";
import { useLingui } from "@lingui/react";
import { useMemo } from "react";
import { composeCoverLetter, createCoverLetterResumeData } from "@reactive-resume/resume/cover-letter";
/**
@@ -12,22 +11,19 @@ import { composeCoverLetter, createCoverLetterResumeData } from "@reactive-resum
export function useLetterWords(): LetterWords {
const { i18n } = useLingui();
return useMemo(
() => ({
greeting: (name: string) => t`Dear ${name},`,
teamGreeting: t`Dear hiring team,`,
hiringTeam: t`Hiring team`,
signOff: t`Kind regards,`,
formatDate: (date: string) =>
new Date(`${date}T12:00:00Z`).toLocaleDateString(i18n.locale, {
day: "numeric",
month: "long",
year: "numeric",
timeZone: "UTC",
}),
}),
[i18n.locale],
);
return {
greeting: (name: string) => t`Dear ${name},`,
teamGreeting: t`Dear hiring team,`,
hiringTeam: t`Hiring team`,
signOff: t`Kind regards,`,
formatDate: (date: string) =>
new Date(`${date}T12:00:00Z`).toLocaleDateString(i18n.locale, {
day: "numeric",
month: "long",
year: "numeric",
timeZone: "UTC",
}),
};
}
/** The letter as a one-section document the PDF renderer draws: sender header, recipient, greeting, body, sign-off. */
@@ -4,7 +4,6 @@ import type { CSSProperties } from "react";
import type { CheckIssue } from "./issues";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { useMemo } from "react";
import { getProposalState } from "@reactive-resume/resume/proposals";
import { Button } from "@reactive-resume/ui/components/button";
import { Icon } from "@reactive-resume/ui/components/icon";
@@ -95,10 +94,7 @@ export function CheckPageLayer({ pageIndex, pageMap }: CheckPageLayerProps) {
const breakpoint = useBreakpoint();
const page = pageMap?.pages[pageIndex];
const highlighted = useMemo(
() => (check && highlightTerm && tab === "match" ? targetsMentioning(check.data, highlightTerm) : []),
[check, highlightTerm, tab],
);
const highlighted = check && highlightTerm && tab === "match" ? targetsMentioning(check.data, highlightTerm) : [];
if (!check || !pageMap || !page || page.width <= 0 || page.height <= 0) return null;
@@ -22,5 +22,5 @@ export function useCheck(): CheckResult | null {
/** Open issues, for the badge on the Check tab. Ignored ones don't count. */
export function useOpenIssueCount(): number {
const data = useDeferredValue(useResumeData());
return useMemo(() => (data ? lintResumeForAts(data).findings.length : 0), [data]);
return data ? lintResumeForAts(data).findings.length : 0;
}
@@ -5,7 +5,7 @@ import { t } from "@lingui/core/macro";
import { Plural, Trans } from "@lingui/react/macro";
import { useMutation } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { useMemo, useState } from "react";
import { useState } from "react";
import { buildMarkdown } from "@reactive-resume/resume/markdown";
import { collectPassages } from "@reactive-resume/resume/proposals";
import { Button, buttonVariants } from "@reactive-resume/ui/components/button";
@@ -45,17 +45,13 @@ export function WritingTab({ data, issues }: WritingTabProps) {
const setProposals = useEditorStore((state) => state.setProposals);
const provider = usableProviders.find((entry) => entry.id === providerOverride) ?? usableProviders[0];
const passages = useMemo(
() =>
collectPassages(data, {
summary: getSectionName(data, "summary"),
sectionTitle: (sectionId) => getSectionName(data, sectionId),
entryTitle: (type, entry) => describeEntry(type as never, entry as never).title,
bullet: (n) => t`bullet ${n}`,
paragraph: (n) => t`paragraph ${n}`,
}).slice(0, MAX_PASSAGES),
[data],
);
const passages = collectPassages(data, {
summary: getSectionName(data, "summary"),
sectionTitle: (sectionId) => getSectionName(data, sectionId),
entryTitle: (type, entry) => describeEntry(type as never, entry as never).title,
bullet: (n) => t`bullet ${n}`,
paragraph: (n) => t`paragraph ${n}`,
}).slice(0, MAX_PASSAGES);
const { mutate, isPending, error, reset } = useMutation({
...orpc.ai.atsReview.mutationOptions(),
@@ -1,7 +1,7 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { Template } from "@reactive-resume/schema/templates";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useMemo } from "react";
import { useEffect } from "react";
import { createResumePdfBlob } from "@/features/resume/export/pdf-document";
import { createPdfFirstPageImageUrl, releaseThumbnailUrls } from "@/features/resume/preview/pdf-thumbnail";
@@ -41,7 +41,7 @@ function fingerprint(data: ResumeData) {
*/
export function useTemplateThumbnail(template: Template, data: ResumeData, enabled: boolean) {
const queryClient = useQueryClient();
const hash = useMemo(() => fingerprint(data), [data]);
const hash = fingerprint(data);
useEffect(() => {
releaseThumbnailUrls(queryClient, QUERY_KEY);
@@ -2,7 +2,6 @@ import { t } from "@lingui/core/macro";
import { Plural, Trans } from "@lingui/react/macro";
import { useMutation } from "@tanstack/react-query";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { useMemo } from "react";
import { Alert, AlertDescription } from "@reactive-resume/ui/components/alert";
import { Button } from "@reactive-resume/ui/components/button";
import { Icon } from "@reactive-resume/ui/components/icon";
@@ -31,7 +30,7 @@ export function WritePanel() {
const locale = useCurrentBuilderResumeSelector((resume) => resume.data.metadata.page.locale);
// The selector hook reads `undefined` as "no resume yet", so optional values need a fallback.
const dateFormat = useCurrentBuilderResumeSelector((resume) => resume.data.metadata.page.dateFormat ?? null);
const page = useMemo(() => ({ locale, dateFormat: dateFormat ?? undefined }), [locale, dateFormat]);
const page = { locale, dateFormat: dateFormat ?? undefined };
const added = useEditorStore((state) => state.addedSections);
const isEmpty = useCurrentBuilderResumeSelector((resume) => getOutlineRows(resume.data, new Set(added)).length === 0);
const openDialog = useDialogStore((state) => state.openDialog);
@@ -77,7 +76,7 @@ function ImportedNote() {
const { imported } = useSearch({ strict: false }) as { imported?: string };
const navigate = useNavigate();
const data = useCurrentBuilderResumeSelector((resume) => resume.data);
const { sections, entries, toCheck } = useMemo(() => summarizeContent(data), [data]);
const { sections, entries, toCheck } = summarizeContent(data);
if (!imported) return null;
const dismiss = () =>
@@ -1,6 +1,5 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { IconName } from "@reactive-resume/ui/components/icon";
import { useMemo } from "react";
import { getResumeSectionTitle } from "@reactive-resume/pdf/section-title";
import { Icon } from "@reactive-resume/ui/components/icon";
import { contrastOnWhite } from "@reactive-resume/utils/color";
@@ -108,7 +107,7 @@ type ResumeReflowProps = { data: ResumeData };
* font. Contact details are tap targets. The PDF stays the exact page.
*/
export function ResumeReflow({ data }: ResumeReflowProps) {
const order = useMemo(() => reflowOrder(data), [data]);
const order = reflowOrder(data);
const { basics } = data;
// The template's colour, darkened where it would be too faint to read on white.
const primary = data.metadata.design.colors.primary;
@@ -1,5 +1,5 @@
import { t } from "@lingui/core/macro";
import { startTransition, useCallback } from "react";
import { startTransition } from "react";
import { Button } from "@reactive-resume/ui/components/button";
import { Icon } from "@reactive-resume/ui/components/icon";
import { useTheme } from "./provider";
@@ -7,11 +7,11 @@ import { useTheme } from "./provider";
export function ThemeToggleButton(props: React.ComponentProps<typeof Button>) {
const { resolvedTheme, toggleTheme } = useTheme();
const onToggleTheme = useCallback(() => {
const onToggleTheme = () => {
startTransition(() => {
toggleTheme();
});
}, [toggleTheme]);
};
const ariaLabel = resolvedTheme === "dark" ? t`Switch to light theme` : t`Switch to dark theme`;
@@ -64,16 +64,4 @@ describe("useControlledState", () => {
rerender({ value: 2 });
expect(onChange).not.toHaveBeenCalled();
});
it("returns a stable setter reference when onChange is stable", () => {
const onChange = vi.fn();
const { result, rerender } = renderHook(
({ value }: { value: number }) => useControlledState<number>({ value, onChange }),
{ initialProps: { value: 1 } },
);
const initialSetter = result.current[1];
rerender({ value: 2 });
expect(result.current[1]).toBe(initialSetter);
});
});
+5 -8
View File
@@ -1,4 +1,4 @@
import { useCallback, useState } from "react";
import { useState } from "react";
interface CommonControlledStateProps<T> {
value?: T;
@@ -18,13 +18,10 @@ export function useControlledState<T, Rest extends unknown[] = []>(
const [internalState, setInternalState] = useState<T>(value !== undefined ? value : (defaultValue as T));
const state = isControlled ? (value as T) : internalState;
const setState = useCallback(
(next: T, ...args: Rest) => {
if (!isControlled) setInternalState(next);
onChange?.(next, ...args);
},
[isControlled, onChange],
);
const setState = (next: T, ...args: Rest) => {
if (!isControlled) setInternalState(next);
onChange?.(next, ...args);
};
return [state, setState] as const;
}
+7 -7
View File
@@ -1,7 +1,7 @@
import type { AnyFormApi } from "@tanstack/react-form";
import { t } from "@lingui/core/macro";
import { useStore } from "@tanstack/react-form";
import { useCallback, useEffect, useRef } from "react";
import { useEffect, useRef } from "react";
import { useDialogStore } from "@/dialogs/store";
import { useConfirm } from "@/hooks/use-confirm";
@@ -22,12 +22,12 @@ export function useFormBlocker(form: Pick<AnyFormApi, "store">, options?: UseFor
shouldBlockRef.current = options?.shouldBlock;
}, [options?.shouldBlock]);
const shouldBlock = useCallback(() => {
const shouldBlock = () => {
if (shouldBlockRef.current) return shouldBlockRef.current();
return isDirty && !isSubmitting;
}, [isDirty, isSubmitting]);
};
const confirmClose = useCallback(() => {
const confirmClose = () => {
if (!shouldBlock()) return true;
return confirm(t`Are you sure you want to close this dialog?`, {
@@ -35,14 +35,14 @@ export function useFormBlocker(form: Pick<AnyFormApi, "store">, options?: UseFor
confirmText: t`Leave`,
cancelText: t`Stay`,
});
}, [shouldBlock, confirm]);
};
const requestClose = useCallback(async () => {
const requestClose = async () => {
const confirmed = await confirmClose();
if (!confirmed) return;
closeDialog();
}, [confirmClose, closeDialog]);
};
useEffect(() => {
setOnBeforeClose(confirmClose);
@@ -3,7 +3,7 @@ import { t } from "@lingui/core/macro";
import { useLingui } from "@lingui/react";
import { Plural, Trans } from "@lingui/react/macro";
import { useQuery } from "@tanstack/react-query";
import { useMemo, useState } from "react";
import { useState } from "react";
import { Icon } from "@reactive-resume/ui/components/icon";
import { useBreakpoint } from "@reactive-resume/ui/hooks/use-breakpoint";
import { cn } from "@reactive-resume/utils/style";
@@ -65,25 +65,18 @@ export function PageCanvas() {
// Check → Writing's and the assistant's proposed edits show on the page, the old text struck through and the new
// highlighted.
const markEdits = mode === "check" && checkTab === "writing" && pageView === "page" && proposals.length > 0;
const marked = useMemo(
() => [...(markEdits ? proposals : []), ...assistantProposals],
[markEdits, proposals, assistantProposals],
);
const marked = [...(markEdits ? proposals : []), ...assistantProposals];
const pendingOnPage = data ? pendingProposals(data, assistantProposals).length : 0;
const parser = mode === "check" && pageView === "parser" && !viewing;
// Design: a hovered or focused template is drawn on the page until it's applied or the pointer leaves.
const previewData = useMemo(
() =>
viewing
? viewing.data
: data && previewTemplate
? { ...data, metadata: { ...data.metadata, template: previewTemplate } }
: data && marked.length > 0
? markProposals(data, marked)
: undefined,
[data, previewTemplate, viewing, marked],
);
const previewData = viewing
? viewing.data
: data && previewTemplate
? { ...data, metadata: { ...data.metadata, template: previewTemplate } }
: data && marked.length > 0
? markProposals(data, marked)
: undefined;
const overflow = data && !previewTemplate && !viewing ? measureOverflow(data, rendered) : null;
// Desktop: the page moves 120px aside so it stays visible beside the Share & export sheet.
const shifted = sheetOpen && (breakpoint === "desktop" || breakpoint === "wide");
@@ -3,7 +3,7 @@ import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { useQuery } from "@tanstack/react-query";
import { createFileRoute, stripSearchParams, useNavigate } from "@tanstack/react-router";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useState } from "react";
import z from "zod";
import { Button } from "@reactive-resume/ui/components/button";
import {
@@ -92,10 +92,7 @@ function RouteComponent() {
}, [applicationId, applications, navigate]);
const text = query.trim().toLowerCase();
const filtered = useMemo(
() => (applications ?? []).filter((application) => matches(application, text)),
[applications, text],
);
const filtered = (applications ?? []).filter((application) => matches(application, text));
const selected = applications?.find((application) => application.id === selectedId) ?? null;
// The board needs room to drag: phones get the list instead.
const shown: View = phone && (view === "board" || view === "calendar") ? "list" : view;