From 1a15d2c03990a45a0560958ea066662dd4eb8c78 Mon Sep 17 00:00:00 2001
From: Matiss Janis Aboltins <matiss@mja.lv>
Date: Sun, 3 Sep 2023 21:56:58 +0100
Subject: [PATCH] :recycle:  use useCategories everywhere (#1597)

---
 .../src/components/ManageRules.js             | 12 +++++-
 .../desktop-client/src/components/Modals.tsx  |  4 +-
 .../src/components/accounts/Account.js        |  7 ++-
 .../src/components/budget/MobileBudget.js     |  4 +-
 .../src/components/budget/index.js            |  3 +-
 .../src/components/modals/EditField.js        |  3 +-
 .../components/payees/ManagePayeesWithData.js |  3 +-
 .../src/components/rules/Value.tsx            | 43 ++++++++-----------
 .../transactions/MobileTransaction.js         | 37 ++++++++++------
 .../transactions/SimpleTransactionsTable.js   |  5 ++-
 .../src/components/util/GenericInput.js       | 13 +++---
 upcoming-release-notes/1597.md                |  6 +++
 12 files changed, 81 insertions(+), 59 deletions(-)
 create mode 100644 upcoming-release-notes/1597.md

diff --git a/packages/desktop-client/src/components/ManageRules.js b/packages/desktop-client/src/components/ManageRules.js
index 98b433d4f..11c9e3737 100644
--- a/packages/desktop-client/src/components/ManageRules.js
+++ b/packages/desktop-client/src/components/ManageRules.js
@@ -14,6 +14,7 @@ import * as undo from 'loot-core/src/platform/client/undo';
 import { mapField, friendlyOp } from 'loot-core/src/shared/rules';
 import { describeSchedule } from 'loot-core/src/shared/schedules';
 
+import useCategories from '../hooks/useCategories';
 import useSelected, { SelectedProvider } from '../hooks/useSelected';
 import { theme } from '../style';
 
@@ -88,12 +89,19 @@ function ManageRulesContent({ isModal, payeeId, setLoading }) {
   let dispatch = useDispatch();
 
   let { data: schedules } = SchedulesQuery.useQuery();
-  let filterData = useSelector(state => ({
+  let { list: categories } = useCategories();
+  let state = useSelector(state => ({
     payees: state.queries.payees,
-    categories: state.queries.categories.list,
     accounts: state.queries.accounts,
     schedules,
   }));
+  let filterData = useMemo(
+    () => ({
+      ...state,
+      categories,
+    }),
+    [state, categories],
+  );
 
   let filteredRules = useMemo(
     () =>
diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx
index 7299f17ab..2dc2423bd 100644
--- a/packages/desktop-client/src/components/Modals.tsx
+++ b/packages/desktop-client/src/components/Modals.tsx
@@ -5,6 +5,7 @@ import { useLocation } from 'react-router-dom';
 import { send } from 'loot-core/src/platform/client/fetch';
 
 import { useActions } from '../hooks/useActions';
+import useCategories from '../hooks/useCategories';
 import useSyncServerStatus from '../hooks/useSyncServerStatus';
 import { type CommonModalProps } from '../types/modals';
 
@@ -34,8 +35,7 @@ export default function Modals() {
   const modalStack = useSelector(state => state.modals.modalStack);
   const isHidden = useSelector(state => state.modals.isHidden);
   const accounts = useSelector(state => state.queries.accounts);
-  const categoryGroups = useSelector(state => state.queries.categories.grouped);
-  const categories = useSelector(state => state.queries.categories.list);
+  const { grouped: categoryGroups, list: categories } = useCategories();
   const budgetId = useSelector(
     state => state.prefs.local && state.prefs.local.id,
   );
diff --git a/packages/desktop-client/src/components/accounts/Account.js b/packages/desktop-client/src/components/accounts/Account.js
index 796b97f15..da79a1fc5 100644
--- a/packages/desktop-client/src/components/accounts/Account.js
+++ b/packages/desktop-client/src/components/accounts/Account.js
@@ -26,6 +26,7 @@ import {
 import { applyChanges, groupById } from 'loot-core/src/shared/util';
 
 import { authorizeBank } from '../../gocardless';
+import useCategories from '../../hooks/useCategories';
 import { SelectedProviderWithItems } from '../../hooks/useSelected';
 import { styles, theme } from '../../style';
 import Button from '../common/Button';
@@ -272,9 +273,6 @@ class AccountInternal extends PureComponent {
 
     // Important that any async work happens last so that the
     // listeners are set up synchronously
-    if (this.props.categoryGroups.length === 0) {
-      await this.props.getCategories();
-    }
     await this.props.initiallyLoadPayees();
     await this.fetchTransactions();
 
@@ -1350,12 +1348,12 @@ export default function Account() {
   let params = useParams();
   let location = useLocation();
 
+  let { grouped: categoryGroups } = useCategories();
   let state = useSelector(state => ({
     newTransactions: state.queries.newTransactions,
     matchedTransactions: state.queries.matchedTransactions,
     accounts: state.queries.accounts,
     failedAccounts: state.account.failedAccounts,
-    categoryGroups: state.queries.categories.grouped,
     dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy',
     hideFraction: state.prefs.local.hideFraction || false,
     expandSplits: state.prefs.local['expand-splits'],
@@ -1408,6 +1406,7 @@ export default function Account() {
       >
         <AccountHack
           {...state}
+          categoryGroups={categoryGroups}
           {...actionCreators}
           modalShowing={state.modalShowing}
           accountId={params.id}
diff --git a/packages/desktop-client/src/components/budget/MobileBudget.js b/packages/desktop-client/src/components/budget/MobileBudget.js
index 0486a5b32..559d42f42 100644
--- a/packages/desktop-client/src/components/budget/MobileBudget.js
+++ b/packages/desktop-client/src/components/budget/MobileBudget.js
@@ -11,6 +11,7 @@ import {
 import * as monthUtils from 'loot-core/src/shared/months';
 
 import { useActions } from '../../hooks/useActions';
+import useCategories from '../../hooks/useCategories';
 import { useSetThemeColor } from '../../hooks/useSetThemeColor';
 import AnimatedLoading from '../../icons/AnimatedLoading';
 import { theme } from '../../style';
@@ -293,8 +294,7 @@ class Budget extends Component {
 }
 
 export default function BudgetWrapper() {
-  let categoryGroups = useSelector(state => state.queries.categories.grouped);
-  let categories = useSelector(state => state.queries.categories.list);
+  let { list: categories, grouped: categoryGroups } = useCategories();
   let budgetType = useSelector(
     state => state.prefs.local.budgetType || 'rollover',
   );
diff --git a/packages/desktop-client/src/components/budget/index.js b/packages/desktop-client/src/components/budget/index.js
index d5a0497c9..cbaa70ed4 100644
--- a/packages/desktop-client/src/components/budget/index.js
+++ b/packages/desktop-client/src/components/budget/index.js
@@ -24,6 +24,7 @@ import {
 import * as monthUtils from 'loot-core/src/shared/months';
 
 import { useActions } from '../../hooks/useActions';
+import useCategories from '../../hooks/useCategories';
 import useFeatureFlag from '../../hooks/useFeatureFlag';
 import { styles } from '../../style';
 import View from '../common/View';
@@ -468,7 +469,7 @@ export default function BudgetWrapper(props) {
     state => state.prefs.local.budgetType || 'rollover',
   );
   let maxMonths = useSelector(state => state.prefs.global.maxMonths);
-  let categoryGroups = useSelector(state => state.queries.categories.grouped);
+  let { grouped: categoryGroups } = useCategories();
 
   let actions = useActions();
   let spreadsheet = useSpreadsheet();
diff --git a/packages/desktop-client/src/components/modals/EditField.js b/packages/desktop-client/src/components/modals/EditField.js
index f4632dafe..eee4d0eb4 100644
--- a/packages/desktop-client/src/components/modals/EditField.js
+++ b/packages/desktop-client/src/components/modals/EditField.js
@@ -7,6 +7,7 @@ import { currentDay, dayFromDate } from 'loot-core/src/shared/months';
 import { amountToInteger } from 'loot-core/src/shared/util';
 
 import { useActions } from '../../hooks/useActions';
+import useCategories from '../../hooks/useCategories';
 import { useResponsive } from '../../ResponsiveProvider';
 import { colors } from '../../style';
 import AccountAutocomplete from '../autocomplete/AccountAutocomplete';
@@ -22,7 +23,7 @@ export default function EditField({ modalProps, name, onSubmit }) {
   let dateFormat = useSelector(
     state => state.prefs.local.dateFormat || 'MM/dd/yyyy',
   );
-  let categoryGroups = useSelector(state => state.queries.categories.grouped);
+  let { grouped: categoryGroups } = useCategories();
   let accounts = useSelector(state => state.queries.accounts);
   let payees = useSelector(state => state.queries.payees);
 
diff --git a/packages/desktop-client/src/components/payees/ManagePayeesWithData.js b/packages/desktop-client/src/components/payees/ManagePayeesWithData.js
index 01cced13d..a99364df5 100644
--- a/packages/desktop-client/src/components/payees/ManagePayeesWithData.js
+++ b/packages/desktop-client/src/components/payees/ManagePayeesWithData.js
@@ -5,13 +5,14 @@ import { send, listen } from 'loot-core/src/platform/client/fetch';
 import { applyChanges } from 'loot-core/src/shared/util';
 
 import { useActions } from '../../hooks/useActions';
+import useCategories from '../../hooks/useCategories';
 
 import { ManagePayees } from '.';
 
 export default function ManagePayeesWithData({ initialSelectedIds }) {
   let initialPayees = useSelector(state => state.queries.payees);
   let lastUndoState = useSelector(state => state.app.lastUndoState);
-  let categoryGroups = useSelector(state => state.queries.categories.grouped);
+  let { grouped: categoryGroups } = useCategories();
 
   let { initiallyLoadPayees, getPayees, setLastUndoState, pushModal } =
     useActions();
diff --git a/packages/desktop-client/src/components/rules/Value.tsx b/packages/desktop-client/src/components/rules/Value.tsx
index e7d24448c..74474f1ec 100644
--- a/packages/desktop-client/src/components/rules/Value.tsx
+++ b/packages/desktop-client/src/components/rules/Value.tsx
@@ -7,6 +7,7 @@ import { getMonthYearFormat } from 'loot-core/src/shared/months';
 import { getRecurringDescription } from 'loot-core/src/shared/schedules';
 import { integerToCurrency } from 'loot-core/src/shared/util';
 
+import useCategories from '../../hooks/useCategories';
 import { theme } from '../../style';
 import LinkButton from '../common/LinkButton';
 import Text from '../common/Text';
@@ -33,31 +34,23 @@ export default function Value<T>({
   // @ts-expect-error fix this later
   describe = x => x.name,
 }: ValueProps<T>) {
-  let { data, dateFormat } = useSelector(state => {
-    let data;
-    if (dataProp) {
-      data = dataProp;
-    } else {
-      switch (field) {
-        case 'payee':
-          data = state.queries.payees;
-          break;
-        case 'category':
-          data = state.queries.categories.list;
-          break;
-        case 'account':
-          data = state.queries.accounts;
-          break;
-        default:
-          data = [];
-      }
-    }
+  let dateFormat = useSelector(
+    state => state.prefs.local.dateFormat || 'MM/dd/yyyy',
+  );
+  let payees = useSelector(state => state.queries.payees);
+  let { list: categories } = useCategories();
+  let accounts = useSelector(state => state.queries.accounts);
+
+  let data =
+    dataProp ||
+    (field === 'payee'
+      ? payees
+      : field === 'category'
+      ? categories
+      : field === 'account'
+      ? accounts
+      : []);
 
-    return {
-      data,
-      dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy',
-    };
-  });
   let [expanded, setExpanded] = useState(false);
 
   function onExpand(e) {
@@ -98,7 +91,7 @@ export default function Value<T>({
           if (valueIsRaw) {
             return value;
           }
-          if (data && data.length) {
+          if (data && Array.isArray(data)) {
             let item = data.find(item => item.id === value);
             if (item) {
               return describe(item);
diff --git a/packages/desktop-client/src/components/transactions/MobileTransaction.js b/packages/desktop-client/src/components/transactions/MobileTransaction.js
index 26a9e6a78..97caeeec2 100644
--- a/packages/desktop-client/src/components/transactions/MobileTransaction.js
+++ b/packages/desktop-client/src/components/transactions/MobileTransaction.js
@@ -6,7 +6,7 @@ import React, {
   useState,
   useRef,
 } from 'react';
-import { connect } from 'react-redux';
+import { useSelector } from 'react-redux';
 import { useNavigate, useParams, Link } from 'react-router-dom';
 
 import { useFocusRing } from '@react-aria/focus';
@@ -23,7 +23,6 @@ import {
 import { css } from 'glamor';
 import memoizeOne from 'memoize-one';
 
-import * as actions from 'loot-core/src/client/actions';
 import q, { runQuery } from 'loot-core/src/client/query-helpers';
 import { send } from 'loot-core/src/platform/client/fetch';
 import * as monthUtils from 'loot-core/src/shared/months';
@@ -43,6 +42,8 @@ import {
   groupById,
 } from 'loot-core/src/shared/util';
 
+import { useActions } from '../../hooks/useActions';
+import useCategories from '../../hooks/useCategories';
 import { useSetThemeColor } from '../../hooks/useSetThemeColor';
 import SvgAdd from '../../icons/v1/Add';
 import CheveronLeft from '../../icons/v1/CheveronLeft';
@@ -847,16 +848,28 @@ function TransactionEditUnconnected(props) {
   );
 }
 
-export const TransactionEdit = connect(
-  state => ({
-    categories: state.queries.categories.list,
-    payees: state.queries.payees,
-    lastTransaction: state.queries.lastTransaction,
-    accounts: state.queries.accounts,
-    dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy',
-  }),
-  actions,
-)(TransactionEditUnconnected);
+export const TransactionEdit = props => {
+  const { list: categories } = useCategories();
+  const payees = useSelector(state => state.queries.payees);
+  const lastTransaction = useSelector(state => state.queries.lastTransaction);
+  const accounts = useSelector(state => state.queries.accounts);
+  const dateFormat = useSelector(
+    state => state.prefs.local.dateFormat || 'MM/dd/yyyy',
+  );
+  const actions = useActions();
+
+  return (
+    <TransactionEditUnconnected
+      {...props}
+      {...actions}
+      categories={categories}
+      payees={payees}
+      lastTransaction={lastTransaction}
+      accounts={accounts}
+      dateFormat={dateFormat}
+    />
+  );
+};
 
 class Transaction extends PureComponent {
   render() {
diff --git a/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.js b/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.js
index c171270bf..82165e158 100644
--- a/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.js
+++ b/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.js
@@ -13,6 +13,7 @@ import {
 } from 'loot-core/src/client/reducers/queries';
 import { integerToCurrency } from 'loot-core/src/shared/util';
 
+import useCategories from '../../hooks/useCategories';
 import { useSelectedItems, useSelectedDispatch } from '../../hooks/useSelected';
 import ArrowsSynchronize from '../../icons/v2/ArrowsSynchronize';
 import { theme, styles } from '../../style';
@@ -141,11 +142,11 @@ export default function SimpleTransactionsTable({
   fields = ['date', 'payee', 'amount'],
   style,
 }) {
-  let { payees, categories, accounts, dateFormat } = useSelector(state => {
+  let { grouped: categories } = useCategories();
+  let { payees, accounts, dateFormat } = useSelector(state => {
     return {
       payees: state.queries.payees,
       accounts: state.queries.accounts,
-      categories: state.queries.categories.grouped,
       dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy',
     };
   });
diff --git a/packages/desktop-client/src/components/util/GenericInput.js b/packages/desktop-client/src/components/util/GenericInput.js
index 15e0120c7..bbc7b8d3d 100644
--- a/packages/desktop-client/src/components/util/GenericInput.js
+++ b/packages/desktop-client/src/components/util/GenericInput.js
@@ -3,6 +3,7 @@ import { useSelector } from 'react-redux';
 
 import { getMonthYearFormat } from 'loot-core/src/shared/months';
 
+import useCategories from '../../hooks/useCategories';
 import AccountAutocomplete from '../autocomplete/AccountAutocomplete';
 import Autocomplete from '../autocomplete/Autocomplete';
 import CategoryAutocomplete from '../autocomplete/CategoryAutocomplete';
@@ -24,13 +25,11 @@ export default function GenericInput({
   style,
   onChange,
 }) {
-  let { saved, categoryGroups, dateFormat } = useSelector(state => {
-    return {
-      saved: state.queries.saved,
-      categoryGroups: state.queries.categories.grouped,
-      dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy',
-    };
-  });
+  let { grouped: categoryGroups } = useCategories();
+  let saved = useSelector(state => state.queries.saved);
+  let dateFormat = useSelector(
+    state => state.prefs.local.dateFormat || 'MM/dd/yyyy',
+  );
 
   // This makes the UI more resilient in case of faulty data
   if (multi && !Array.isArray(value)) {
diff --git a/upcoming-release-notes/1597.md b/upcoming-release-notes/1597.md
new file mode 100644
index 000000000..ca53dc34b
--- /dev/null
+++ b/upcoming-release-notes/1597.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [MatissJanis]
+---
+
+Use `useCategories` hook everywhere categories are accessed.
-- 
GitLab