refactor: optimize codebase for React 19 compiler (v1.2.6)

React 19 compiler auto-optimizes memoization, making manual hooks unnecessary.

Changes:
- Remove ~60 useCallback/useMemo across 16 files
- Remove React.memo from nav-button and return-button
- Simplify hydration with useSyncExternalStore (privacy-provider)
- Add CHANGELOG.md for version tracking

No functional changes - internal optimization only.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-02-04 13:14:10 +00:00
parent a70a83dd9d
commit 757626c468
18 changed files with 571 additions and 617 deletions

62
CHANGELOG.md Normal file
View File

@@ -0,0 +1,62 @@
# Changelog
Todas as mudanças notáveis deste projeto serão documentadas neste arquivo.
O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/),
e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/).
## [1.2.6] - 2025-02-04
### Alterado
- Refatoração para otimização do React 19 compiler
- Removidos `useCallback` e `useMemo` desnecessários (~60 instâncias)
- Removidos `React.memo` wrappers desnecessários
- Simplificados padrões de hidratação com `useSyncExternalStore`
### Arquivos modificados
- `hooks/use-calculator-state.ts`
- `hooks/use-form-state.ts`
- `hooks/use-month-period.ts`
- `components/auth/signup-form.tsx`
- `components/contas/accounts-page.tsx`
- `components/contas/transfer-dialog.tsx`
- `components/lancamentos/table/lancamentos-filters.tsx`
- `components/sidebar/nav-main.tsx`
- `components/month-picker/nav-button.tsx`
- `components/month-picker/return-button.tsx`
- `components/privacy-provider.tsx`
- `components/dashboard/category-history-widget.tsx`
- `components/anotacoes/note-dialog.tsx`
- `components/categorias/category-dialog.tsx`
- `components/confirm-action-dialog.tsx`
- `components/orcamentos/budget-dialog.tsx`
## [1.2.5] - 2025-02-01
### Adicionado
- Widget de pagadores no dashboard
- Avatares atualizados para pagadores
## [1.2.4] - 2025-01-22
### Corrigido
- Preservar formatação nas anotações
- Layout do card de anotações
## [1.2.3] - 2025-01-22
### Adicionado
- Versão exibida na sidebar
- Documentação atualizada
## [1.2.2] - 2025-01-22
### Alterado
- Atualização de dependências
- Aplicada formatação no código

View File

@@ -3,7 +3,6 @@
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react";
import {
type ReactNode,
useCallback,
useEffect,
useRef,
useState,
@@ -121,24 +120,18 @@ export function NoteDialog({
const disableSubmit = isPending || onlySpaces || unchanged || invalidLen;
const handleOpenChange = useCallback(
(v: boolean) => {
const handleOpenChange = (v: boolean) => {
setDialogOpen(v);
if (!v) setErrorMessage(null);
},
[setDialogOpen],
);
};
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
const handleKeyDown = (e: React.KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter")
(e.currentTarget as HTMLFormElement).requestSubmit();
if (e.key === "Escape") handleOpenChange(false);
},
[handleOpenChange],
);
};
const handleAddTask = useCallback(() => {
const handleAddTask = () => {
const text = normalize(newTaskText);
if (!text) return;
@@ -151,32 +144,25 @@ export function NoteDialog({
updateField("tasks", [...(formState.tasks || []), newTask]);
setNewTaskText("");
requestAnimationFrame(() => newTaskRef.current?.focus());
}, [newTaskText, formState.tasks, updateField]);
};
const handleRemoveTask = useCallback(
(taskId: string) => {
const handleRemoveTask = (taskId: string) => {
updateField(
"tasks",
(formState.tasks || []).filter((t) => t.id !== taskId),
);
},
[formState.tasks, updateField],
);
};
const handleToggleTask = useCallback(
(taskId: string) => {
const handleToggleTask = (taskId: string) => {
updateField(
"tasks",
(formState.tasks || []).map((t) =>
t.id === taskId ? { ...t, completed: !t.completed } : t,
),
);
},
[formState.tasks, updateField],
);
};
const handleSubmit = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setErrorMessage(null);
@@ -228,20 +214,7 @@ export function NoteDialog({
toast.error(result.error);
titleRef.current?.focus();
});
},
[
formState.title,
formState.description,
formState.type,
formState.tasks,
mode,
note,
setDialogOpen,
onlySpaces,
unchanged,
invalidLen,
],
);
};
return (
<Dialog open={dialogOpen} onOpenChange={handleOpenChange}>

View File

@@ -1,7 +1,7 @@
"use client";
import { RiCheckLine, RiCloseLine, RiLoader4Line } from "@remixicon/react";
import { useRouter } from "next/navigation";
import { type FormEvent, useMemo, useState } from "react";
import { type FormEvent, useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
@@ -90,10 +90,7 @@ export function SignupForm({ className, ...props }: DivProps) {
const [loadingEmail, setLoadingEmail] = useState(false);
const [loadingGoogle, setLoadingGoogle] = useState(false);
const passwordValidation = useMemo(
() => validatePassword(password),
[password],
);
const passwordValidation = validatePassword(password);
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();

View File

@@ -1,12 +1,6 @@
"use client";
import {
useCallback,
useEffect,
useMemo,
useState,
useTransition,
} from "react";
import { useEffect, useState, useTransition } from "react";
import { toast } from "sonner";
import {
createCategoryAction,
@@ -76,14 +70,10 @@ export function CategoryDialog({
onOpenChange,
);
const initialState = useMemo(
() =>
buildInitialValues({
const initialState = buildInitialValues({
category,
defaultType,
}),
[category, defaultType],
);
});
// Use form state hook for form management
const { formState, updateField, setFormState } =
@@ -95,7 +85,7 @@ export function CategoryDialog({
setFormState(initialState);
setErrorMessage(null);
}
}, [dialogOpen, initialState, setFormState]);
}, [dialogOpen, setFormState, category, defaultType]);
// Clear error when dialog closes
useEffect(() => {
@@ -104,8 +94,7 @@ export function CategoryDialog({
}
}, [dialogOpen]);
const handleSubmit = useCallback(
(event: React.FormEvent<HTMLFormElement>) => {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setErrorMessage(null);
@@ -141,9 +130,7 @@ export function CategoryDialog({
setErrorMessage(result.error);
toast.error(result.error);
});
},
[category?.id, formState, initialState, mode, setDialogOpen, setFormState],
);
};
const title = mode === "create" ? "Nova categoria" : "Editar categoria";
const description =

View File

@@ -1,7 +1,7 @@
"use client";
import type { VariantProps } from "class-variance-authority";
import { useCallback, useMemo, useState, useTransition } from "react";
import { useState, useTransition } from "react";
import {
AlertDialog,
AlertDialogAction,
@@ -49,22 +49,16 @@ export function ConfirmActionDialog({
const [isPending, startTransition] = useTransition();
const dialogOpen = open ?? internalOpen;
const setDialogOpen = useCallback(
(value: boolean) => {
const setDialogOpen = (value: boolean) => {
if (open === undefined) {
setInternalOpen(value);
}
onOpenChange?.(value);
},
[onOpenChange, open],
);
};
const resolvedPendingLabel = useMemo(
() => pendingLabel ?? confirmLabel,
[pendingLabel, confirmLabel],
);
const resolvedPendingLabel = pendingLabel ?? confirmLabel;
const handleConfirm = useCallback(() => {
const handleConfirm = () => {
if (!onConfirm) {
setDialogOpen(false);
return;
@@ -78,7 +72,7 @@ export function ConfirmActionDialog({
// Mantém o diálogo aberto para que o chamador trate o erro.
}
});
}, [onConfirm, setDialogOpen]);
};
return (
<AlertDialog open={dialogOpen} onOpenChange={setDialogOpen}>

View File

@@ -3,7 +3,7 @@
import { RiAddCircleLine, RiBankLine } from "@remixicon/react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useCallback, useMemo, useState } from "react";
import { useState } from "react";
import { toast } from "sonner";
import { deleteAccountAction } from "@/app/(dashboard)/contas/actions";
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
@@ -47,8 +47,7 @@ export function AccountsPage({
const hasAccounts = accounts.length > 0;
const orderedAccounts = useMemo(() => {
return [...accounts].sort((a, b) => {
const orderedAccounts = [...accounts].sort((a, b) => {
// Coloca inativas no final
const aIsInactive = a.status?.toLowerCase() === "inativa";
const bIsInactive = b.status?.toLowerCase() === "inativa";
@@ -59,33 +58,32 @@ export function AccountsPage({
// Mesma ordem alfabética dentro de cada grupo
return a.name.localeCompare(b.name, "pt-BR", { sensitivity: "base" });
});
}, [accounts]);
const handleEdit = useCallback((account: Account) => {
const handleEdit = (account: Account) => {
setSelectedAccount(account);
setEditOpen(true);
}, []);
};
const handleEditOpenChange = useCallback((open: boolean) => {
const handleEditOpenChange = (open: boolean) => {
setEditOpen(open);
if (!open) {
setSelectedAccount(null);
}
}, []);
};
const handleRemoveRequest = useCallback((account: Account) => {
const handleRemoveRequest = (account: Account) => {
setAccountToRemove(account);
setRemoveOpen(true);
}, []);
};
const handleRemoveOpenChange = useCallback((open: boolean) => {
const handleRemoveOpenChange = (open: boolean) => {
setRemoveOpen(open);
if (!open) {
setAccountToRemove(null);
}
}, []);
};
const handleRemoveConfirm = useCallback(async () => {
const handleRemoveConfirm = async () => {
if (!accountToRemove) {
return;
}
@@ -99,19 +97,19 @@ export function AccountsPage({
toast.error(result.error);
throw new Error(result.error);
}, [accountToRemove]);
};
const handleTransferRequest = useCallback((account: Account) => {
const handleTransferRequest = (account: Account) => {
setTransferFromAccount(account);
setTransferOpen(true);
}, []);
};
const handleTransferOpenChange = useCallback((open: boolean) => {
const handleTransferOpenChange = (open: boolean) => {
setTransferOpen(open);
if (!open) {
setTransferFromAccount(null);
}
}, []);
};
const removeTitle = accountToRemove
? `Remover conta "${accountToRemove.name}"?`

View File

@@ -1,6 +1,6 @@
"use client";
import { useMemo, useState, useTransition } from "react";
import { useState, useTransition } from "react";
import { toast } from "sonner";
import { transferBetweenAccountsAction } from "@/app/(dashboard)/contas/actions";
import type { AccountData } from "@/app/(dashboard)/contas/data";
@@ -62,15 +62,13 @@ export function TransferDialog({
const [period, setPeriod] = useState(currentPeriod);
// Available destination accounts (exclude source account)
const availableAccounts = useMemo(
() => accounts.filter((account) => account.id !== fromAccountId),
[accounts, fromAccountId],
const availableAccounts = accounts.filter(
(account) => account.id !== fromAccountId,
);
// Source account info
const fromAccount = useMemo(
() => accounts.find((account) => account.id === fromAccountId),
[accounts, fromAccountId],
const fromAccount = accounts.find(
(account) => account.id === fromAccountId,
);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {

View File

@@ -4,7 +4,7 @@ import {
RiBarChartBoxLine,
RiCloseLine,
} from "@remixicon/react";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
@@ -49,11 +49,14 @@ export function CategoryHistoryWidget({ data }: CategoryHistoryWidgetProps) {
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [isClient, setIsClient] = useState(false);
const [open, setOpen] = useState(false);
const isFirstRender = useRef(true);
// Load selected categories from sessionStorage on mount
// Load from sessionStorage on mount and save on changes
useEffect(() => {
setIsClient(true);
// Only load from storage on first render
if (isFirstRender.current) {
const stored = sessionStorage.getItem(STORAGE_KEY_SELECTED);
if (stored) {
try {
@@ -68,17 +71,15 @@ export function CategoryHistoryWidget({ data }: CategoryHistoryWidgetProps) {
// Invalid JSON, ignore
}
}
}, [data.allCategories]);
// Save to sessionStorage when selection changes
useEffect(() => {
if (isClient) {
isFirstRender.current = false;
} else {
// Save to storage on subsequent changes
sessionStorage.setItem(
STORAGE_KEY_SELECTED,
JSON.stringify(selectedCategories),
);
}
}, [selectedCategories, isClient]);
}, [selectedCategories, data.allCategories]);
// Filter data to show only selected categories with vibrant colors
const filteredCategories = useMemo(() => {

View File

@@ -6,14 +6,7 @@ import {
RiFilter3Line,
} from "@remixicon/react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import {
type ReactNode,
useCallback,
useEffect,
useMemo,
useState,
useTransition,
} from "react";
import { type ReactNode, useEffect, useState, useTransition } from "react";
import { Button } from "@/components/ui/button";
import {
Command,
@@ -147,13 +140,10 @@ export function LancamentosFilters({
const searchParams = useSearchParams();
const [isPending, startTransition] = useTransition();
const getParamValue = useCallback(
(key: string) => searchParams.get(key) ?? FILTER_EMPTY_VALUE,
[searchParams],
);
const getParamValue = (key: string) =>
searchParams.get(key) ?? FILTER_EMPTY_VALUE;
const handleFilterChange = useCallback(
(key: string, value: string | null) => {
const handleFilterChange = (key: string, value: string | null) => {
const nextParams = new URLSearchParams(searchParams.toString());
if (value && value !== FILTER_EMPTY_VALUE) {
@@ -167,9 +157,7 @@ export function LancamentosFilters({
scroll: false,
});
});
},
[pathname, router, searchParams],
);
};
const [searchValue, setSearchValue] = useState(searchParams.get("q") ?? "");
const currentSearchParam = searchParams.get("q") ?? "";
@@ -191,7 +179,7 @@ export function LancamentosFilters({
return () => clearTimeout(timeout);
}, [searchValue, currentSearchParam, handleFilterChange]);
const handleReset = useCallback(() => {
const handleReset = () => {
const periodValue = searchParams.get("periodo");
const nextParams = new URLSearchParams();
if (periodValue) {
@@ -205,41 +193,29 @@ export function LancamentosFilters({
: pathname;
router.replace(target, { scroll: false });
});
}, [pathname, router, searchParams]);
};
const pagadorSelectOptions = useMemo(
() =>
pagadorOptions.map((option) => ({
const pagadorSelectOptions = pagadorOptions.map((option) => ({
value: option.slug,
label: option.label,
avatarUrl: option.avatarUrl,
})),
[pagadorOptions],
);
}));
const contaOptions = useMemo(
() =>
contaCartaoOptions
const contaOptions = contaCartaoOptions
.filter((option) => option.kind === "conta")
.map((option) => ({
value: option.slug,
label: option.label,
logo: option.logo,
})),
[contaCartaoOptions],
);
}));
const cartaoOptions = useMemo(
() =>
contaCartaoOptions
const cartaoOptions = contaCartaoOptions
.filter((option) => option.kind === "cartao")
.map((option) => ({
value: option.slug,
label: option.label,
logo: option.logo,
})),
[contaCartaoOptions],
);
}));
const categoriaValue = getParamValue("categoria");
const selectedCategoria =
@@ -262,21 +238,18 @@ export function LancamentosFilters({
const [categoriaOpen, setCategoriaOpen] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
const hasActiveFilters = useMemo(() => {
return (
const hasActiveFilters =
searchParams.get("transacao") ||
searchParams.get("condicao") ||
searchParams.get("pagamento") ||
searchParams.get("pagador") ||
searchParams.get("categoria") ||
searchParams.get("contaCartao")
);
}, [searchParams]);
searchParams.get("contaCartao");
const handleResetFilters = useCallback(() => {
const handleResetFilters = () => {
handleReset();
setDrawerOpen(false);
}, [handleReset]);
};
return (
<div className={cn("flex flex-wrap items-center gap-2", className)}>

View File

@@ -1,7 +1,6 @@
"use client";
import { RiArrowLeftSLine, RiArrowRightSLine } from "@remixicon/react";
import React from "react";
interface NavigationButtonProps {
direction: "left" | "right";
@@ -9,8 +8,11 @@ interface NavigationButtonProps {
onClick: () => void;
}
const NavigationButton = React.memo(
({ direction, disabled, onClick }: NavigationButtonProps) => {
export default function NavigationButton({
direction,
disabled,
onClick,
}: NavigationButtonProps) {
const Icon = direction === "left" ? RiArrowLeftSLine : RiArrowRightSLine;
return (
@@ -25,9 +27,4 @@ const NavigationButton = React.memo(
<Icon className="text-primary" size={18} />
</button>
);
},
);
NavigationButton.displayName = "NavigationButton";
export default NavigationButton;
}

View File

@@ -1,6 +1,5 @@
"use client";
import React from "react";
import { Button } from "../ui/button";
interface ReturnButtonProps {
@@ -8,7 +7,7 @@ interface ReturnButtonProps {
onClick: () => void;
}
const ReturnButton = React.memo(({ disabled, onClick }: ReturnButtonProps) => {
export default function ReturnButton({ disabled, onClick }: ReturnButtonProps) {
return (
<Button
className="w-32 h-6 rounded-sm lowercase"
@@ -20,8 +19,4 @@ const ReturnButton = React.memo(({ disabled, onClick }: ReturnButtonProps) => {
Ir para Mês Atual
</Button>
);
});
ReturnButton.displayName = "ReturnButton";
export default ReturnButton;
}

View File

@@ -1,12 +1,6 @@
"use client";
import {
useCallback,
useEffect,
useMemo,
useState,
useTransition,
} from "react";
import { useEffect, useState, useTransition } from "react";
import { toast } from "sonner";
import {
createBudgetAction,
@@ -79,14 +73,10 @@ export function BudgetDialog({
onOpenChange,
);
const initialState = useMemo(
() =>
buildInitialValues({
const initialState = buildInitialValues({
budget,
defaultPeriod,
}),
[budget, defaultPeriod],
);
});
// Use form state hook for form management
const { formState, updateField, setFormState } =
@@ -98,7 +88,7 @@ export function BudgetDialog({
setFormState(initialState);
setErrorMessage(null);
}
}, [dialogOpen, initialState, setFormState]);
}, [dialogOpen, setFormState, budget, defaultPeriod]);
// Clear error when dialog closes
useEffect(() => {
@@ -107,8 +97,7 @@ export function BudgetDialog({
}
}, [dialogOpen]);
const handleSubmit = useCallback(
(event: React.FormEvent<HTMLFormElement>) => {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setErrorMessage(null);
@@ -165,9 +154,7 @@ export function BudgetDialog({
setErrorMessage(result.error);
toast.error(result.error);
});
},
[budget?.id, formState, initialState, mode, setDialogOpen, setFormState],
);
};
const title = mode === "create" ? "Novo orçamento" : "Editar orçamento";
const description =

View File

@@ -1,7 +1,14 @@
"use client";
import type React from "react";
import { createContext, useContext, useEffect, useState } from "react";
import {
createContext,
useContext,
useEffect,
useRef,
useState,
useSyncExternalStore,
} from "react";
interface PrivacyContextType {
privacyMode: boolean;
@@ -13,25 +20,41 @@ const PrivacyContext = createContext<PrivacyContextType | undefined>(undefined);
const STORAGE_KEY = "app:privacyMode";
// Read from localStorage safely (returns false on server)
function getStoredValue(): boolean {
if (typeof window === "undefined") return false;
return localStorage.getItem(STORAGE_KEY) === "true";
}
// Subscribe to storage changes
function subscribeToStorage(callback: () => void) {
window.addEventListener("storage", callback);
return () => window.removeEventListener("storage", callback);
}
export function PrivacyProvider({ children }: { children: React.ReactNode }) {
const [privacyMode, setPrivacyMode] = useState(false);
const [hydrated, setHydrated] = useState(false);
// useSyncExternalStore handles hydration safely
const storedValue = useSyncExternalStore(
subscribeToStorage,
getStoredValue,
() => false, // Server snapshot
);
// Sincronizar com localStorage na montagem (evitar mismatch SSR/CSR)
const [privacyMode, setPrivacyMode] = useState(storedValue);
const isFirstRender = useRef(true);
// Sync with stored value on mount
useEffect(() => {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored !== null) {
setPrivacyMode(stored === "true");
if (isFirstRender.current) {
setPrivacyMode(storedValue);
isFirstRender.current = false;
}
setHydrated(true);
}, []);
}, [storedValue]);
// Persistir mudanças no localStorage
// Persist to localStorage when privacyMode changes
useEffect(() => {
if (hydrated) {
localStorage.setItem(STORAGE_KEY, String(privacyMode));
}
}, [privacyMode, hydrated]);
}, [privacyMode]);
const toggle = () => {
setPrivacyMode((prev) => !prev);

View File

@@ -63,12 +63,12 @@ export function NavMain({ sections }: { sections: NavSection[] }) {
const searchParams = useSearchParams();
const periodParam = searchParams.get(MONTH_PERIOD_PARAM);
const isLinkActive = React.useCallback(
(url: string) => {
const normalizedPathname =
pathname.endsWith("/") && pathname !== "/"
? pathname.slice(0, -1)
: pathname;
const isLinkActive = (url: string) => {
const normalizedUrl =
url.endsWith("/") && url !== "/" ? url.slice(0, -1) : url;
@@ -77,23 +77,20 @@ export function NavMain({ sections }: { sections: NavSection[] }) {
normalizedPathname === normalizedUrl ||
normalizedPathname.startsWith(`${normalizedUrl}/`)
);
},
[pathname],
);
};
const buildHrefWithPeriod = React.useCallback(
(url: string) => {
const buildHrefWithPeriod = (url: string) => {
if (!periodParam) {
return url;
}
const [rawPathname, existingSearch = ""] = url.split("?");
const normalizedPathname =
const normalizedRawPathname =
rawPathname.endsWith("/") && rawPathname !== "/"
? rawPathname.slice(0, -1)
: rawPathname;
if (!PERIOD_AWARE_PATHS.has(normalizedPathname)) {
if (!PERIOD_AWARE_PATHS.has(normalizedRawPathname)) {
return url;
}
@@ -102,11 +99,9 @@ export function NavMain({ sections }: { sections: NavSection[] }) {
const queryString = params.toString();
return queryString
? `${normalizedPathname}?${queryString}`
: normalizedPathname;
},
[periodParam],
);
? `${normalizedRawPathname}?${queryString}`
: normalizedRawPathname;
};
const activeLinkClasses =
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-dark! hover:text-primary!";

View File

@@ -1,5 +1,5 @@
import type { VariantProps } from "class-variance-authority";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import type { buttonVariants } from "@/components/ui/button";
import {
formatLocaleValue,
@@ -26,9 +26,9 @@ export function useCalculatorState() {
const [copied, setCopied] = useState(false);
const resetCopiedTimeoutRef = useRef<number | undefined>(undefined);
const currentValue = useMemo(() => Number(display), [display]);
const currentValue = Number(display);
const resultText = useMemo(() => {
const resultText = (() => {
if (display === "Erro") {
return null;
}
@@ -39,18 +39,17 @@ export function useCalculatorState() {
}
return formatLocaleValue(normalized);
}, [currentValue, display]);
})();
const reset = useCallback(() => {
const reset = () => {
setDisplay("0");
setAccumulator(null);
setOperator(null);
setOverwrite(false);
setHistory(null);
}, []);
};
const inputDigit = useCallback(
(digit: string) => {
const inputDigit = (digit: string) => {
// Check conditions before state updates
const shouldReset = overwrite || display === "Erro";
@@ -77,11 +76,9 @@ export function useCalculatorState() {
setOverwrite(false);
setHistory(null);
}
},
[overwrite, display],
);
};
const inputDecimal = useCallback(() => {
const inputDecimal = () => {
// Check conditions before state updates
const shouldReset = overwrite || display === "Erro";
@@ -108,10 +105,9 @@ export function useCalculatorState() {
setOverwrite(false);
setHistory(null);
}
}, [overwrite, display]);
};
const setNextOperator = useCallback(
(nextOperator: Operator) => {
const setNextOperator = (nextOperator: Operator) => {
if (display === "Erro") {
reset();
return;
@@ -137,11 +133,9 @@ export function useCalculatorState() {
setOperator(nextOperator);
setOverwrite(true);
setHistory(null);
},
[accumulator, currentValue, display, operator, overwrite, reset],
);
};
const evaluate = useCallback(() => {
const evaluate = () => {
if (operator === null || accumulator === null || display === "Erro") {
return;
}
@@ -161,9 +155,9 @@ export function useCalculatorState() {
setOperator(null);
setOverwrite(true);
setHistory(operation);
}, [accumulator, currentValue, display, operator]);
};
const toggleSign = useCallback(() => {
const toggleSign = () => {
setDisplay((prev) => {
if (prev === "Erro") {
return prev;
@@ -177,9 +171,9 @@ export function useCalculatorState() {
setOverwrite(false);
setHistory(null);
}
}, [overwrite]);
};
const deleteLastDigit = useCallback(() => {
const deleteLastDigit = () => {
setHistory(null);
// Check conditions before state updates
@@ -209,9 +203,9 @@ export function useCalculatorState() {
} else if (overwrite) {
setOverwrite(false);
}
}, [overwrite, display]);
};
const applyPercent = useCallback(() => {
const applyPercent = () => {
setDisplay((prev) => {
if (prev === "Erro") {
return prev;
@@ -221,9 +215,9 @@ export function useCalculatorState() {
});
setOverwrite(true);
setHistory(null);
}, []);
};
const expression = useMemo(() => {
const expression = (() => {
if (display === "Erro") {
return "Erro";
}
@@ -240,13 +234,12 @@ export function useCalculatorState() {
}
return formatLocaleValue(display);
}, [accumulator, display, operator, overwrite]);
})();
const buttons = useMemo(() => {
const makeOperatorHandler = (nextOperator: Operator) => () =>
setNextOperator(nextOperator);
return [
const buttons: CalculatorButtonConfig[][] = [
[
{ label: "C", onClick: reset, variant: "destructive" },
{ label: "⌫", onClick: deleteLastDigit, variant: "default" },
@@ -289,19 +282,9 @@ export function useCalculatorState() {
{ label: ",", onClick: inputDecimal },
{ label: "=", onClick: evaluate, variant: "default" },
],
] satisfies CalculatorButtonConfig[][];
}, [
applyPercent,
deleteLastDigit,
evaluate,
inputDecimal,
inputDigit,
reset,
setNextOperator,
toggleSign,
]);
];
const copyToClipboard = useCallback(async () => {
const copyToClipboard = async () => {
if (!resultText) return;
try {
@@ -320,9 +303,9 @@ export function useCalculatorState() {
error,
);
}
}, [resultText]);
};
const pasteFromClipboard = useCallback(async () => {
const pasteFromClipboard = async () => {
if (!navigator.clipboard?.readText) return;
try {
@@ -364,7 +347,7 @@ export function useCalculatorState() {
} catch (error) {
console.error("Não foi possível colar o valor na calculadora.", error);
}
}, []);
};
useEffect(() => {
return () => {

View File

@@ -1,4 +1,4 @@
import { useCallback, useState } from "react";
import { useState } from "react";
/**
* Hook for managing form state with type-safe field updates
@@ -22,26 +22,23 @@ export function useFormState<T extends Record<string, any>>(initialValues: T) {
/**
* Updates a single field in the form state
*/
const updateField = useCallback(
<K extends keyof T>(field: K, value: T[K]) => {
const updateField = <K extends keyof T>(field: K, value: T[K]) => {
setFormState((prev) => ({ ...prev, [field]: value }));
},
[],
);
};
/**
* Resets form to initial values
*/
const resetForm = useCallback(() => {
const resetForm = () => {
setFormState(initialValues);
}, [initialValues]);
};
/**
* Updates multiple fields at once
*/
const updateFields = useCallback((updates: Partial<T>) => {
const updateFields = (updates: Partial<T>) => {
setFormState((prev) => ({ ...prev, ...updates }));
}, []);
};
return {
formState,

View File

@@ -1,7 +1,7 @@
"use client";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useCallback, useMemo } from "react";
import { useMemo } from "react";
import { MONTH_NAMES } from "@/lib/utils/period";
@@ -46,8 +46,7 @@ export function useMonthPeriod() {
};
}, [periodFromParams, defaultMonth, defaultYear, optionsMeses]);
const buildHref = useCallback(
(month: string, year: string | number) => {
const buildHref = (month: string, year: string | number) => {
const normalizedMonth = normalizeMonth(month);
const normalizedYear = String(year).trim();
@@ -55,20 +54,15 @@ export function useMonthPeriod() {
params.set(PERIOD_PARAM, `${normalizedMonth}-${normalizedYear}`);
return `${pathname}?${params.toString()}`;
},
[pathname, searchParams],
);
};
const replacePeriod = useCallback(
(target: string) => {
const replacePeriod = (target: string) => {
if (!target) {
return;
}
router.replace(target, { scroll: false });
},
[router],
);
};
return {
monthNames: optionsMeses,

View File

@@ -1,6 +1,6 @@
{
"name": "opensheets",
"version": "1.2.5",
"version": "1.2.6",
"private": true,
"scripts": {
"dev": "next dev --turbopack",