From 218aad51a878aa7d5d45bc9a8c1ebf2b8e2b8719 Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Tue, 29 Sep 2026 10:50:26 +0200 Subject: [PATCH] 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. --- .../web/src/components/input/color-picker.tsx | 3 +- apps/web/src/components/input/icon-picker.tsx | 4 +-- apps/web/src/components/input/url-input.tsx | 25 ++++++---------- apps/web/src/components/level/combobox.tsx | 11 +++---- .../src/components/typography/combobox.tsx | 18 ++++------- apps/web/src/components/ui/donation-toast.tsx | 10 +++---- .../applications/components/calendar-view.tsx | 12 ++++---- .../components/import-applications-sheet.tsx | 11 ++++--- .../applications/components/insights-view.tsx | 24 +++++---------- apps/web/src/features/assistant/markdown.tsx | 5 ++-- apps/web/src/features/letters/compose.ts | 30 ++++++++----------- .../resume/editor/check/page-layer.tsx | 6 +--- .../features/resume/editor/check/use-check.ts | 2 +- .../resume/editor/check/writing-tab.tsx | 20 +++++-------- .../resume/editor/design/thumbnails.ts | 4 +-- .../resume/editor/write/write-panel.tsx | 5 ++-- .../features/resume/public/resume-reflow.tsx | 3 +- apps/web/src/features/theme/toggle-button.tsx | 6 ++-- .../src/hooks/use-controlled-state.test.tsx | 12 -------- apps/web/src/hooks/use-controlled-state.tsx | 13 ++++---- apps/web/src/hooks/use-form-blocker.tsx | 14 ++++----- .../$resumeId/-components/page-canvas.tsx | 25 ++++++---------- .../routes/dashboard/applications/index.tsx | 7 ++--- 23 files changed, 101 insertions(+), 169 deletions(-) diff --git a/apps/web/src/components/input/color-picker.tsx b/apps/web/src/components/input/color-picker.tsx index 5586ad914..ae63c31a4 100644 --- a/apps/web/src/components/input/color-picker.tsx +++ b/apps/web/src/components/input/color-picker.tsx @@ -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); diff --git a/apps/web/src/components/input/icon-picker.tsx b/apps/web/src/components/input/icon-picker.tsx index a6077f2f0..42666c16d 100644 --- a/apps/web/src/components/input/icon-picker.tsx +++ b/apps/web/src/components/input/icon-picker.tsx @@ -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, "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 ( diff --git a/apps/web/src/components/input/url-input.tsx b/apps/web/src/components/input/url-input.tsx index 885f68c5e..119ac1323 100644 --- a/apps/web/src/components/input/url-input.tsx +++ b/apps/web/src/components/input/url-input.tsx @@ -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 = Omit({ value, onChange, hideLabelButton, ...props }: Props) { const prefix = value.url.match(HTTP_PREFIX)?.[0] ?? DEFAULT_PREFIX; - const handleUrlChange = useCallback( - (e: React.ChangeEvent) => { - onChange({ - ...value, - url: ensurePrefix(e.target.value, prefix), - }); - }, - [onChange, value, prefix], - ); + const handleUrlChange = (e: React.ChangeEvent) => { + onChange({ + ...value, + url: ensurePrefix(e.target.value, prefix), + }); + }; - const handleLabelChange = useCallback( - (e: React.ChangeEvent) => { - onChange({ ...value, label: e.target.value }); - }, - [onChange, value], - ); + const handleLabelChange = (e: React.ChangeEvent) => { + onChange({ ...value, label: e.target.value }); + }; const urlValue = stripPrefix(value.url); diff --git a/apps/web/src/components/level/combobox.tsx b/apps/web/src/components/level/combobox.tsx index 59bf8647f..dd9b9429f 100644 --- a/apps/web/src/components/level/combobox.tsx +++ b/apps/web/src/components/level/combobox.tsx @@ -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 ; } diff --git a/apps/web/src/components/typography/combobox.tsx b/apps/web/src/components/typography/combobox.tsx index 2a0b28be2..3ec0a5cb0 100644 --- a/apps/web/src/components/typography/combobox.tsx +++ b/apps/web/src/components/typography/combobox.tsx @@ -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 ( (() => 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); diff --git a/apps/web/src/features/applications/components/calendar-view.tsx b/apps/web/src/features/applications/components/calendar-view.tsx index fda1e469e..e72e344d1 100644 --- a/apps/web/src/features/applications/components/calendar-view.tsx +++ b/apps/web/src/features/applications/components/calendar-view.tsx @@ -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({ 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); diff --git a/apps/web/src/features/applications/components/import-applications-sheet.tsx b/apps/web/src/features/applications/components/import-applications-sheet.tsx index 744c6d21a..373a18e08 100644 --- a/apps/web/src/features/applications/components/import-applications-sheet.tsx +++ b/apps/web/src/features/applications/components/import-applications-sheet.tsx @@ -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>({}); const fileRef = useRef(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) : []; diff --git a/apps/web/src/features/applications/components/insights-view.tsx b/apps/web/src/features/applications/components/insights-view.tsx index 11ec89dfa..fcce989ba 100644 --- a/apps/web/src/features/applications/components/insights-view.tsx +++ b/apps/web/src/features/applications/components/insights-view.tsx @@ -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)); diff --git a/apps/web/src/features/assistant/markdown.tsx b/apps/web/src/features/assistant/markdown.tsx index f19057802..aa1f992c7 100644 --- a/apps/web/src/features/assistant/markdown.tsx +++ b/apps/web/src/features/assistant/markdown.tsx @@ -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 ( ); -}); +}; diff --git a/apps/web/src/features/letters/compose.ts b/apps/web/src/features/letters/compose.ts index c5842572c..0b81da79d 100644 --- a/apps/web/src/features/letters/compose.ts +++ b/apps/web/src/features/letters/compose.ts @@ -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. */ diff --git a/apps/web/src/features/resume/editor/check/page-layer.tsx b/apps/web/src/features/resume/editor/check/page-layer.tsx index c7bdb703c..58b8d283d 100644 --- a/apps/web/src/features/resume/editor/check/page-layer.tsx +++ b/apps/web/src/features/resume/editor/check/page-layer.tsx @@ -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; diff --git a/apps/web/src/features/resume/editor/check/use-check.ts b/apps/web/src/features/resume/editor/check/use-check.ts index af48e26ed..49c8fc230 100644 --- a/apps/web/src/features/resume/editor/check/use-check.ts +++ b/apps/web/src/features/resume/editor/check/use-check.ts @@ -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; } diff --git a/apps/web/src/features/resume/editor/check/writing-tab.tsx b/apps/web/src/features/resume/editor/check/writing-tab.tsx index 993d0a95a..d27927ec0 100644 --- a/apps/web/src/features/resume/editor/check/writing-tab.tsx +++ b/apps/web/src/features/resume/editor/check/writing-tab.tsx @@ -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(), diff --git a/apps/web/src/features/resume/editor/design/thumbnails.ts b/apps/web/src/features/resume/editor/design/thumbnails.ts index ca99600c1..b19bada76 100644 --- a/apps/web/src/features/resume/editor/design/thumbnails.ts +++ b/apps/web/src/features/resume/editor/design/thumbnails.ts @@ -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); diff --git a/apps/web/src/features/resume/editor/write/write-panel.tsx b/apps/web/src/features/resume/editor/write/write-panel.tsx index b4ed818cc..86ac1c5e0 100644 --- a/apps/web/src/features/resume/editor/write/write-panel.tsx +++ b/apps/web/src/features/resume/editor/write/write-panel.tsx @@ -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 = () => diff --git a/apps/web/src/features/resume/public/resume-reflow.tsx b/apps/web/src/features/resume/public/resume-reflow.tsx index 18acd745b..27c31213a 100644 --- a/apps/web/src/features/resume/public/resume-reflow.tsx +++ b/apps/web/src/features/resume/public/resume-reflow.tsx @@ -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; diff --git a/apps/web/src/features/theme/toggle-button.tsx b/apps/web/src/features/theme/toggle-button.tsx index 548f64749..753f67bbe 100644 --- a/apps/web/src/features/theme/toggle-button.tsx +++ b/apps/web/src/features/theme/toggle-button.tsx @@ -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) { 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`; diff --git a/apps/web/src/hooks/use-controlled-state.test.tsx b/apps/web/src/hooks/use-controlled-state.test.tsx index fbd9f78ff..22df30fd1 100644 --- a/apps/web/src/hooks/use-controlled-state.test.tsx +++ b/apps/web/src/hooks/use-controlled-state.test.tsx @@ -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({ value, onChange }), - { initialProps: { value: 1 } }, - ); - - const initialSetter = result.current[1]; - rerender({ value: 2 }); - expect(result.current[1]).toBe(initialSetter); - }); }); diff --git a/apps/web/src/hooks/use-controlled-state.tsx b/apps/web/src/hooks/use-controlled-state.tsx index a118aaef0..0c2d0eaf4 100644 --- a/apps/web/src/hooks/use-controlled-state.tsx +++ b/apps/web/src/hooks/use-controlled-state.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from "react"; +import { useState } from "react"; interface CommonControlledStateProps { value?: T; @@ -18,13 +18,10 @@ export function useControlledState( const [internalState, setInternalState] = useState(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; } diff --git a/apps/web/src/hooks/use-form-blocker.tsx b/apps/web/src/hooks/use-form-blocker.tsx index b9c0621e2..8176a72d4 100644 --- a/apps/web/src/hooks/use-form-blocker.tsx +++ b/apps/web/src/hooks/use-form-blocker.tsx @@ -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, 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, 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); diff --git a/apps/web/src/routes/builder/$resumeId/-components/page-canvas.tsx b/apps/web/src/routes/builder/$resumeId/-components/page-canvas.tsx index 8eb3c358c..86c28cbe5 100644 --- a/apps/web/src/routes/builder/$resumeId/-components/page-canvas.tsx +++ b/apps/web/src/routes/builder/$resumeId/-components/page-canvas.tsx @@ -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"); diff --git a/apps/web/src/routes/dashboard/applications/index.tsx b/apps/web/src/routes/dashboard/applications/index.tsx index f2f94ad63..4765419a7 100644 --- a/apps/web/src/routes/dashboard/applications/index.tsx +++ b/apps/web/src/routes/dashboard/applications/index.tsx @@ -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;