From bdbf6e9ca6de99aca2d96ff7845cf2132de1d37e Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Sun, 15 Sep 2024 15:10:52 -0700 Subject: [PATCH] [Maintenance] Reduce budget table re-renders (#3448) * Reduce budget table re-renders * Release notes --- .../src/components/mobile/budget/index.tsx | 498 ++++++++++-------- upcoming-release-notes/3448.md | 6 + 2 files changed, 279 insertions(+), 225 deletions(-) create mode 100644 upcoming-release-notes/3448.md diff --git a/packages/desktop-client/src/components/mobile/budget/index.tsx b/packages/desktop-client/src/components/mobile/budget/index.tsx index 65eb9543fd6..cb89707082c 100644 --- a/packages/desktop-client/src/components/mobile/budget/index.tsx +++ b/packages/desktop-client/src/components/mobile/budget/index.tsx @@ -1,5 +1,5 @@ // @ts-strict-ignore -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; import { @@ -20,10 +20,6 @@ import { import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider'; import { send, listen } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; -import { - type CategoryEntity, - type CategoryGroupEntity, -} from 'loot-core/src/types/models'; import { useCategories } from '../../../hooks/useCategories'; import { useLocalPref } from '../../../hooks/useLocalPref'; @@ -43,15 +39,13 @@ function isBudgetType(input?: string): input is 'rollover' | 'report' { return ['rollover', 'report'].includes(input); } -type BudgetInnerProps = { - categories: CategoryEntity[]; - categoryGroups: CategoryGroupEntity[]; - budgetType: 'rollover' | 'report'; - spreadsheet: ReturnType; -}; +export function Budget() { + useSetThemeColor(theme.mobileViewTheme); -function BudgetInner(props: BudgetInnerProps) { - const { categoryGroups, categories, budgetType, spreadsheet } = props; + const { list: categories, grouped: categoryGroups } = useCategories(); + const [budgetTypePref] = useMetadataPref('budgetType'); + const budgetType = isBudgetType(budgetTypePref) ? budgetTypePref : 'rollover'; + const spreadsheet = useSpreadsheet(); const currMonth = monthUtils.currentMonth(); const [startMonth = currMonth, setStartMonthPref] = @@ -60,9 +54,8 @@ function BudgetInner(props: BudgetInnerProps) { start: startMonth, end: startMonth, }); - const [initialized, setInitialized] = useState(false); // const [editMode, setEditMode] = useState(false); - + const [initialized, setInitialized] = useState(false); const [_numberFormat] = useSyncedPref('numberFormat'); const numberFormat = _numberFormat || 'comma-dot'; const [hideFraction] = useSyncedPref('hideFraction'); @@ -95,11 +88,14 @@ function BudgetInner(props: BudgetInnerProps) { return () => unlisten(); }, [budgetType, startMonth, dispatch, spreadsheet]); - const onBudgetAction = async (month, type, args) => { - dispatch(applyBudgetAction(month, type, args)); - }; + const onBudgetAction = useCallback( + async (month, type, args) => { + dispatch(applyBudgetAction(month, type, args)); + }, + [dispatch], + ); - const onShowBudgetSummary = () => { + const onShowBudgetSummary = useCallback(() => { if (budgetType === 'report') { dispatch( pushModal('report-budget-summary', { @@ -114,9 +110,9 @@ function BudgetInner(props: BudgetInnerProps) { }), ); } - }; + }, [budgetType, dispatch, onBudgetAction, startMonth]); - const onOpenNewCategoryGroupModal = () => { + const onOpenNewCategoryGroupModal = useCallback(() => { dispatch( pushModal('new-category-group', { onValidate: name => (!name ? 'Name is required.' : null), @@ -126,152 +122,180 @@ function BudgetInner(props: BudgetInnerProps) { }, }), ); - }; - - const onOpenNewCategoryModal = (groupId, isIncome) => { - dispatch( - pushModal('new-category', { - onValidate: name => (!name ? 'Name is required.' : null), - onSubmit: async name => { - dispatch(collapseModals('category-group-menu')); - dispatch(createCategory(name, groupId, isIncome, false)); - }, - }), - ); - }; - - const onSaveGroup = group => { - dispatch(updateGroup(group)); - }; - - const onDeleteGroup = async groupId => { - const group = categoryGroups?.find(g => g.id === groupId); - - if (!group) { - return; - } - - let mustTransfer = false; - for (const category of group.categories ?? []) { - if (await send('must-category-transfer', { id: category.id })) { - mustTransfer = true; - break; - } - } + }, [dispatch]); - if (mustTransfer) { + const onOpenNewCategoryModal = useCallback( + (groupId, isIncome) => { dispatch( - pushModal('confirm-category-delete', { - group: groupId, - onDelete: transferCategory => { + pushModal('new-category', { + onValidate: name => (!name ? 'Name is required.' : null), + onSubmit: async name => { dispatch(collapseModals('category-group-menu')); - dispatch(deleteGroup(groupId, transferCategory)); + dispatch(createCategory(name, groupId, isIncome, false)); }, }), ); - } else { - dispatch(collapseModals('category-group-menu')); - dispatch(deleteGroup(groupId)); - } - }; - - const onToggleGroupVisibility = groupId => { - const group = categoryGroups.find(g => g.id === groupId); - onSaveGroup({ - ...group, - hidden: !!!group.hidden, - }); - dispatch(collapseModals('category-group-menu')); - }; + }, + [dispatch], + ); - const onSaveCategory = category => { - dispatch(updateCategory(category)); - }; + const onSaveGroup = useCallback( + group => { + dispatch(updateGroup(group)); + }, + [dispatch], + ); - const onDeleteCategory = async categoryId => { - const mustTransfer = await send('must-category-transfer', { - id: categoryId, - }); + const onDeleteGroup = useCallback( + async groupId => { + const group = categoryGroups?.find(g => g.id === groupId); - if (mustTransfer) { - dispatch( - pushModal('confirm-category-delete', { - category: categoryId, - onDelete: transferCategory => { - if (categoryId !== transferCategory) { - dispatch(collapseModals('category-menu')); - dispatch(deleteCategory(categoryId, transferCategory)); - } - }, - }), - ); - } else { - dispatch(collapseModals('category-menu')); - dispatch(deleteCategory(categoryId)); - } - }; + if (!group) { + return; + } - const onToggleCategoryVisibility = categoryId => { - const category = categories.find(c => c.id === categoryId); - onSaveCategory({ - ...category, - hidden: !!!category.hidden, - }); - dispatch(collapseModals('category-menu')); - }; + let mustTransfer = false; + for (const category of group.categories ?? []) { + if (await send('must-category-transfer', { id: category.id })) { + mustTransfer = true; + break; + } + } - const onReorderCategory = (id, { inGroup, aroundCategory }) => { - let groupId, targetId; + if (mustTransfer) { + dispatch( + pushModal('confirm-category-delete', { + group: groupId, + onDelete: transferCategory => { + dispatch(collapseModals('category-group-menu')); + dispatch(deleteGroup(groupId, transferCategory)); + }, + }), + ); + } else { + dispatch(collapseModals('category-group-menu')); + dispatch(deleteGroup(groupId)); + } + }, + [categoryGroups, dispatch], + ); - if (inGroup) { - groupId = inGroup; - } else if (aroundCategory) { - const { id: originalCatId, position } = aroundCategory; + const onToggleGroupVisibility = useCallback( + groupId => { + const group = categoryGroups.find(g => g.id === groupId); + onSaveGroup({ + ...group, + hidden: !!!group.hidden, + }); + dispatch(collapseModals('category-group-menu')); + }, + [categoryGroups, dispatch, onSaveGroup], + ); - let catId = originalCatId; - const group = categoryGroups.find(group => - group.categories?.find(cat => cat.id === catId), - ); + const onSaveCategory = useCallback( + category => { + dispatch(updateCategory(category)); + }, + [dispatch], + ); - if (position === 'bottom') { - const idx = group?.categories?.findIndex(cat => cat.id === catId) ?? -1; - catId = group?.categories - ? idx < group.categories.length - 1 - ? group.categories[idx + 1].id - : null - : null; + const onDeleteCategory = useCallback( + async categoryId => { + const mustTransfer = await send('must-category-transfer', { + id: categoryId, + }); + + if (mustTransfer) { + dispatch( + pushModal('confirm-category-delete', { + category: categoryId, + onDelete: transferCategory => { + if (categoryId !== transferCategory) { + dispatch(collapseModals('category-menu')); + dispatch(deleteCategory(categoryId, transferCategory)); + } + }, + }), + ); + } else { + dispatch(collapseModals('category-menu')); + dispatch(deleteCategory(categoryId)); } + }, + [dispatch], + ); - groupId = group?.id; - targetId = catId; - } + const onToggleCategoryVisibility = useCallback( + categoryId => { + const category = categories.find(c => c.id === categoryId); + onSaveCategory({ + ...category, + hidden: !!!category.hidden, + }); + dispatch(collapseModals('category-menu')); + }, + [categories, dispatch, onSaveCategory], + ); - dispatch(moveCategory(id, groupId, targetId)); - }; + const onReorderCategory = useCallback( + (id, { inGroup, aroundCategory }) => { + let groupId, targetId; + + if (inGroup) { + groupId = inGroup; + } else if (aroundCategory) { + const { id: originalCatId, position } = aroundCategory; + + let catId = originalCatId; + const group = categoryGroups.find(group => + group.categories?.find(cat => cat.id === catId), + ); + + if (position === 'bottom') { + const idx = + group?.categories?.findIndex(cat => cat.id === catId) ?? -1; + catId = group?.categories + ? idx < group.categories.length - 1 + ? group.categories[idx + 1].id + : null + : null; + } + + groupId = group?.id; + targetId = catId; + } - const onReorderGroup = (id, targetId, position) => { - if (position === 'bottom') { - const idx = categoryGroups.findIndex(group => group.id === targetId); - targetId = - idx < categoryGroups.length - 1 ? categoryGroups[idx + 1].id : null; - } + dispatch(moveCategory(id, groupId, targetId)); + }, + [categoryGroups, dispatch], + ); - dispatch(moveCategoryGroup(id, targetId)); - }; + const onReorderGroup = useCallback( + (id, targetId, position) => { + if (position === 'bottom') { + const idx = categoryGroups.findIndex(group => group.id === targetId); + targetId = + idx < categoryGroups.length - 1 ? categoryGroups[idx + 1].id : null; + } + + dispatch(moveCategoryGroup(id, targetId)); + }, + [categoryGroups, dispatch], + ); - const onPrevMonth = async () => { + const onPrevMonth = useCallback(async () => { const month = monthUtils.subMonths(startMonth, 1); await prewarmMonth(budgetType, spreadsheet, month); setStartMonthPref(month); setInitialized(true); - }; + }, [budgetType, setStartMonthPref, spreadsheet, startMonth]); - const onNextMonth = async () => { + const onNextMonth = useCallback(async () => { const month = monthUtils.addMonths(startMonth, 1); await prewarmMonth(budgetType, spreadsheet, month); setStartMonthPref(month); setInitialized(true); - }; + }, [budgetType, setStartMonthPref, spreadsheet, startMonth]); // const onOpenMonthActionMenu = () => { // const options = [ @@ -312,94 +336,128 @@ function BudgetInner(props: BudgetInnerProps) { // ); // }; - const onSaveNotes = async (id, notes) => { + const onSaveNotes = useCallback(async (id, notes) => { await send('notes-save', { id, note: notes }); - }; + }, []); - const onOpenCategoryGroupNotesModal = id => { - const group = categoryGroups.find(g => g.id === id); - dispatch( - pushModal('notes', { - id, - name: group.name, - onSave: onSaveNotes, - }), - ); - }; + const onOpenCategoryGroupNotesModal = useCallback( + id => { + const group = categoryGroups.find(g => g.id === id); + dispatch( + pushModal('notes', { + id, + name: group.name, + onSave: onSaveNotes, + }), + ); + }, + [categoryGroups, dispatch, onSaveNotes], + ); - const onOpenCategoryNotesModal = id => { - const category = categories.find(c => c.id === id); - dispatch( - pushModal('notes', { - id, - name: category.name, - onSave: onSaveNotes, - }), - ); - }; + const onOpenCategoryNotesModal = useCallback( + id => { + const category = categories.find(c => c.id === id); + dispatch( + pushModal('notes', { + id, + name: category.name, + onSave: onSaveNotes, + }), + ); + }, + [categories, dispatch, onSaveNotes], + ); - const onOpenCategoryGroupMenuModal = id => { - const group = categoryGroups.find(g => g.id === id); - dispatch( - pushModal('category-group-menu', { - groupId: group.id, - onSave: onSaveGroup, - onAddCategory: onOpenNewCategoryModal, - onEditNotes: onOpenCategoryGroupNotesModal, - onDelete: onDeleteGroup, - onToggleVisibility: onToggleGroupVisibility, - }), - ); - }; + const onOpenCategoryGroupMenuModal = useCallback( + id => { + const group = categoryGroups.find(g => g.id === id); + dispatch( + pushModal('category-group-menu', { + groupId: group.id, + onSave: onSaveGroup, + onAddCategory: onOpenNewCategoryModal, + onEditNotes: onOpenCategoryGroupNotesModal, + onDelete: onDeleteGroup, + onToggleVisibility: onToggleGroupVisibility, + }), + ); + }, + [ + categoryGroups, + dispatch, + onDeleteGroup, + onOpenCategoryGroupNotesModal, + onOpenNewCategoryModal, + onSaveGroup, + onToggleGroupVisibility, + ], + ); - const onOpenCategoryMenuModal = id => { - const category = categories.find(c => c.id === id); - dispatch( - pushModal('category-menu', { - categoryId: category.id, - onSave: onSaveCategory, - onEditNotes: onOpenCategoryNotesModal, - onDelete: onDeleteCategory, - onToggleVisibility: onToggleCategoryVisibility, - onBudgetAction, - }), - ); - }; + const onOpenCategoryMenuModal = useCallback( + id => { + const category = categories.find(c => c.id === id); + dispatch( + pushModal('category-menu', { + categoryId: category.id, + onSave: onSaveCategory, + onEditNotes: onOpenCategoryNotesModal, + onDelete: onDeleteCategory, + onToggleVisibility: onToggleCategoryVisibility, + onBudgetAction, + }), + ); + }, + [ + categories, + dispatch, + onBudgetAction, + onDeleteCategory, + onOpenCategoryNotesModal, + onSaveCategory, + onToggleCategoryVisibility, + ], + ); const [showHiddenCategories, setShowHiddenCategoriesPref] = useLocalPref( 'budget.showHiddenCategories', ); - const onToggleHiddenCategories = () => { + const onToggleHiddenCategories = useCallback(() => { setShowHiddenCategoriesPref(!showHiddenCategories); dispatch(collapseModals('budget-page-menu')); - }; + }, [dispatch, setShowHiddenCategoriesPref, showHiddenCategories]); - const onOpenBudgetMonthNotesModal = month => { - dispatch( - pushModal('notes', { - id: `budget-${month}`, - name: monthUtils.format(month, 'MMMM ‘yy'), - onSave: onSaveNotes, - }), - ); - }; + const onOpenBudgetMonthNotesModal = useCallback( + month => { + dispatch( + pushModal('notes', { + id: `budget-${month}`, + name: monthUtils.format(month, 'MMMM ‘yy'), + onSave: onSaveNotes, + }), + ); + }, + [dispatch, onSaveNotes], + ); - const onSwitchBudgetFile = () => { + const onSwitchBudgetFile = useCallback(() => { dispatch(pushModal('budget-list')); - }; + }, [dispatch]); - const onOpenBudgetMonthMenu = month => { - dispatch( - pushModal(`${budgetType}-budget-month-menu`, { - month, - onBudgetAction, - onEditNotes: onOpenBudgetMonthNotesModal, - }), - ); - }; + const onOpenBudgetMonthMenu = useCallback( + month => { + dispatch( + pushModal(`${budgetType}-budget-month-menu`, { + month, + onBudgetAction, + onEditNotes: onOpenBudgetMonthNotesModal, + }), + ); + }, + [budgetType, dispatch, onBudgetAction, onOpenBudgetMonthNotesModal], + ); - const onOpenBudgetPageMenu = () => { + const onOpenBudgetPageMenu = useCallback(() => { dispatch( pushModal('budget-page-menu', { onAddCategoryGroup: onOpenNewCategoryGroupModal, @@ -407,7 +465,12 @@ function BudgetInner(props: BudgetInnerProps) { onSwitchBudgetFile, }), ); - }; + }, [ + dispatch, + onOpenNewCategoryGroupModal, + onSwitchBudgetFile, + onToggleHiddenCategories, + ]); if (!categoryGroups || !initialized) { return ( @@ -464,18 +527,3 @@ function BudgetInner(props: BudgetInnerProps) { ); } - -export function Budget() { - const { list: categories, grouped: categoryGroups } = useCategories(); - const [budgetType] = useMetadataPref('budgetType'); - const spreadsheet = useSpreadsheet(); - useSetThemeColor(theme.mobileViewTheme); - return ( - - ); -} diff --git a/upcoming-release-notes/3448.md b/upcoming-release-notes/3448.md new file mode 100644 index 00000000000..e475df111df --- /dev/null +++ b/upcoming-release-notes/3448.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Reduce budget table re-renders