diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png
index fa4ff40ee97ecb8e257683496581ccbe298620d7..e1ed15d720db70723d09bf378709c32a6c0559ee 100644
Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png
index 4ac36791ac107c7087a25023cfa1651d2a316408..866d234e40ee46bb86bdb7bf2f37d07f5fec9b77 100644
Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png differ
diff --git a/packages/desktop-client/src/components/App.tsx b/packages/desktop-client/src/components/App.tsx
index 696c7633ea18a88a33fcb3f7de46189063317baf..8323f7247212e4009e4d586187831ea93c38ac02 100644
--- a/packages/desktop-client/src/components/App.tsx
+++ b/packages/desktop-client/src/components/App.tsx
@@ -7,17 +7,15 @@ import {
 } from 'react-error-boundary';
 import { useSelector } from 'react-redux';
 
-import { type State } from 'loot-core/client/state-types';
-import { type AppState } from 'loot-core/client/state-types/app';
-import { type PrefsState } from 'loot-core/client/state-types/prefs';
 import * as Platform from 'loot-core/src/client/platform';
+import { type State } from 'loot-core/src/client/state-types';
 import {
   init as initConnection,
   send,
 } from 'loot-core/src/platform/client/fetch';
-import { type GlobalPrefs } from 'loot-core/src/types/prefs';
 
 import { useActions } from '../hooks/useActions';
+import { useLocalPref } from '../hooks/useLocalPref';
 import { installPolyfills } from '../polyfills';
 import { ResponsiveProvider } from '../ResponsiveProvider';
 import { styles, hasHiddenScrollbars, ThemeStyle } from '../style';
@@ -34,26 +32,13 @@ import { UpdateNotification } from './UpdateNotification';
 type AppInnerProps = {
   budgetId: string;
   cloudFileId: string;
-  loadingText: string;
-  loadBudget: (
-    id: string,
-    loadingText?: string,
-    options?: object,
-  ) => Promise<void>;
-  closeBudget: () => Promise<void>;
-  loadGlobalPrefs: () => Promise<GlobalPrefs>;
 };
 
-function AppInner({
-  budgetId,
-  cloudFileId,
-  loadingText,
-  loadBudget,
-  closeBudget,
-  loadGlobalPrefs,
-}: AppInnerProps) {
+function AppInner({ budgetId, cloudFileId }: AppInnerProps) {
   const [initializing, setInitializing] = useState(true);
   const { showBoundary: showErrorBoundary } = useErrorBoundary();
+  const loadingText = useSelector((state: State) => state.app.loadingText);
+  const { loadBudget, closeBudget, loadGlobalPrefs } = useActions();
 
   async function init() {
     const socketName = await global.Actual.getServerSocket();
@@ -126,16 +111,9 @@ function ErrorFallback({ error }: FallbackProps) {
 }
 
 export function App() {
-  const budgetId = useSelector<State, PrefsState['local']['id']>(
-    state => state.prefs.local && state.prefs.local.id,
-  );
-  const cloudFileId = useSelector<State, PrefsState['local']['cloudFileId']>(
-    state => state.prefs.local && state.prefs.local.cloudFileId,
-  );
-  const loadingText = useSelector<State, AppState['loadingText']>(
-    state => state.app.loadingText,
-  );
-  const { loadBudget, closeBudget, loadGlobalPrefs, sync } = useActions();
+  const [budgetId] = useLocalPref('id');
+  const [cloudFileId] = useLocalPref('cloudFileId');
+  const { sync } = useActions();
   const [hiddenScrollbars, setHiddenScrollbars] = useState(
     hasHiddenScrollbars(),
   );
@@ -184,14 +162,7 @@ export function App() {
             {process.env.REACT_APP_REVIEW_ID && !Platform.isPlaywright && (
               <DevelopmentTopBar />
             )}
-            <AppInner
-              budgetId={budgetId}
-              cloudFileId={cloudFileId}
-              loadingText={loadingText}
-              loadBudget={loadBudget}
-              closeBudget={closeBudget}
-              loadGlobalPrefs={loadGlobalPrefs}
-            />
+            <AppInner budgetId={budgetId} cloudFileId={cloudFileId} />
           </ErrorBoundary>
           <ThemeStyle />
         </View>
diff --git a/packages/desktop-client/src/components/BankSyncStatus.tsx b/packages/desktop-client/src/components/BankSyncStatus.tsx
index 3fde066fa4cdafad903645761208277ccf76b4dc..f49b920074996ec221f9a588722b1555fe74ea0a 100644
--- a/packages/desktop-client/src/components/BankSyncStatus.tsx
+++ b/packages/desktop-client/src/components/BankSyncStatus.tsx
@@ -2,8 +2,7 @@ import React from 'react';
 import { useSelector } from 'react-redux';
 import { useTransition, animated } from 'react-spring';
 
-import { type State } from 'loot-core/client/state-types';
-import { type AccountState } from 'loot-core/client/state-types/account';
+import { type State } from 'loot-core/src/client/state-types';
 
 import { theme, styles } from '../style';
 
@@ -12,8 +11,8 @@ import { Text } from './common/Text';
 import { View } from './common/View';
 
 export function BankSyncStatus() {
-  const accountsSyncing = useSelector<State, AccountState['accountsSyncing']>(
-    state => state.account.accountsSyncing,
+  const accountsSyncing = useSelector(
+    (state: State) => state.account.accountsSyncing,
   );
 
   const name = accountsSyncing
diff --git a/packages/desktop-client/src/components/FinancesApp.tsx b/packages/desktop-client/src/components/FinancesApp.tsx
index 29c241a14da54837bb21f34adb8d09310957ca47..8024afe7957e39cffa03a62aca42f5d0105b47d8 100644
--- a/packages/desktop-client/src/components/FinancesApp.tsx
+++ b/packages/desktop-client/src/components/FinancesApp.tsx
@@ -2,6 +2,7 @@
 import React, { type ReactElement, useEffect, useMemo } from 'react';
 import { DndProvider } from 'react-dnd';
 import { HTML5Backend as Backend } from 'react-dnd-html5-backend';
+import { useSelector } from 'react-redux';
 import {
   Route,
   Routes,
@@ -13,12 +14,12 @@ import {
 
 import hotkeys from 'hotkeys-js';
 
-import { AccountsProvider } from 'loot-core/src/client/data-hooks/accounts';
-import { PayeesProvider } from 'loot-core/src/client/data-hooks/payees';
 import { SpreadsheetProvider } from 'loot-core/src/client/SpreadsheetProvider';
+import { type State } from 'loot-core/src/client/state-types';
 import { checkForUpdateNotification } from 'loot-core/src/client/update-notification';
 import * as undo from 'loot-core/src/platform/client/undo';
 
+import { useAccounts } from '../hooks/useAccounts';
 import { useActions } from '../hooks/useActions';
 import { useNavigate } from '../hooks/useNavigate';
 import { useResponsive } from '../ResponsiveProvider';
@@ -39,7 +40,8 @@ import { Reports } from './reports';
 import { NarrowAlternate, WideComponent } from './responsive';
 import { ScrollProvider } from './ScrollProvider';
 import { Settings } from './settings';
-import { FloatableSidebar, SidebarProvider } from './sidebar';
+import { FloatableSidebar } from './sidebar';
+import { SidebarProvider } from './sidebar/SidebarProvider';
 import { Titlebar, TitlebarProvider } from './Titlebar';
 import { TransactionEdit } from './transactions/MobileTransaction';
 
@@ -71,18 +73,19 @@ function WideNotSupported({ children, redirectTo = '/budget' }) {
   return isNarrowWidth ? children : null;
 }
 
-function RouterBehaviors({ getAccounts }) {
+function RouterBehaviors() {
   const navigate = useNavigate();
+  const accounts = useAccounts();
+  const accountsLoaded = useSelector(
+    (state: State) => state.queries.accountsLoaded,
+  );
   useEffect(() => {
-    // Get the accounts and check if any exist. If there are no
-    // accounts, we want to redirect the user to the All Accounts
-    // screen which will prompt them to add an account
-    getAccounts().then(accounts => {
-      if (accounts.length === 0) {
-        navigate('/accounts');
-      }
-    });
-  }, []);
+    // If there are no accounts, we want to redirect the user to
+    // the All Accounts screen which will prompt them to add an account
+    if (accountsLoaded && accounts.length === 0) {
+      navigate('/accounts');
+    }
+  }, [accountsLoaded, accounts]);
 
   const location = useLocation();
   const href = useHref(location);
@@ -116,7 +119,7 @@ function FinancesAppWithoutContext() {
 
   return (
     <BrowserRouter>
-      <RouterBehaviors getAccounts={actions.getAccounts} />
+      <RouterBehaviors />
       <ExposeNavigate />
 
       <View style={{ height: '100%' }}>
@@ -265,13 +268,9 @@ export function FinancesApp() {
       <TitlebarProvider>
         <SidebarProvider>
           <BudgetMonthCountProvider>
-            <PayeesProvider>
-              <AccountsProvider>
-                <DndProvider backend={Backend}>
-                  <ScrollProvider>{app}</ScrollProvider>
-                </DndProvider>
-              </AccountsProvider>
-            </PayeesProvider>
+            <DndProvider backend={Backend}>
+              <ScrollProvider>{app}</ScrollProvider>
+            </DndProvider>
           </BudgetMonthCountProvider>
         </SidebarProvider>
       </TitlebarProvider>
diff --git a/packages/desktop-client/src/components/LoggedInUser.tsx b/packages/desktop-client/src/components/LoggedInUser.tsx
index bfd037ad437cb0653b395e728f0c998225df787e..f1043b764db27d5aa12b18709a9a067d4b6c19dc 100644
--- a/packages/desktop-client/src/components/LoggedInUser.tsx
+++ b/packages/desktop-client/src/components/LoggedInUser.tsx
@@ -2,8 +2,7 @@
 import React, { useState, useEffect } from 'react';
 import { useSelector } from 'react-redux';
 
-import { type State } from 'loot-core/client/state-types';
-import { type UserState } from 'loot-core/client/state-types/user';
+import { type State } from 'loot-core/src/client/state-types';
 
 import { useActions } from '../hooks/useActions';
 import { theme, styles, type CSSProperties } from '../style';
@@ -25,9 +24,7 @@ export function LoggedInUser({
   style,
   color,
 }: LoggedInUserProps) {
-  const userData = useSelector<State, UserState['data']>(
-    state => state.user.data,
-  );
+  const userData = useSelector((state: State) => state.user.data);
   const { getUserData, signOut, closeBudget } = useActions();
   const [loading, setLoading] = useState(true);
   const [menuOpen, setMenuOpen] = useState(false);
diff --git a/packages/desktop-client/src/components/ManageRules.tsx b/packages/desktop-client/src/components/ManageRules.tsx
index 38634a9899a0be7ae8e893c8736e7c8abb3e5b6c..2459fcb44a1415acf3e0b5fef1af3aa7a6ac385d 100644
--- a/packages/desktop-client/src/components/ManageRules.tsx
+++ b/packages/desktop-client/src/components/ManageRules.tsx
@@ -7,10 +7,8 @@ import React, {
   type SetStateAction,
   type Dispatch,
 } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { useDispatch } from 'react-redux';
 
-import { type State } from 'loot-core/client/state-types';
-import { type QueriesState } from 'loot-core/client/state-types/queries';
 import { pushModal } from 'loot-core/src/client/actions/modals';
 import { initiallyLoadPayees } from 'loot-core/src/client/actions/queries';
 import { send } from 'loot-core/src/platform/client/fetch';
@@ -19,7 +17,9 @@ import { mapField, friendlyOp } from 'loot-core/src/shared/rules';
 import { describeSchedule } from 'loot-core/src/shared/schedules';
 import { type RuleEntity } from 'loot-core/src/types/models';
 
+import { useAccounts } from '../hooks/useAccounts';
 import { useCategories } from '../hooks/useCategories';
+import { usePayees } from '../hooks/usePayees';
 import { useSelected, SelectedProvider } from '../hooks/useSelected';
 import { theme } from '../style';
 
@@ -105,18 +105,13 @@ function ManageRulesContent({
 
   const { data: schedules } = SchedulesQuery.useQuery();
   const { list: categories } = useCategories();
-  const state = useSelector<
-    State,
-    {
-      payees: QueriesState['payees'];
-      accounts: QueriesState['accounts'];
-      schedules: ReturnType<(typeof SchedulesQuery)['useQuery']>;
-    }
-  >(state => ({
-    payees: state.queries.payees,
-    accounts: state.queries.accounts,
+  const payees = usePayees();
+  const accounts = useAccounts();
+  const state = {
+    payees,
+    accounts,
     schedules,
-  }));
+  };
   const filterData = useMemo(
     () => ({
       ...state,
diff --git a/packages/desktop-client/src/components/MobileWebMessage.tsx b/packages/desktop-client/src/components/MobileWebMessage.tsx
index bc3e0c7ac93ece0f412ca5cfde33f056b10c5772..c54da6eedf105995c7738f3877896aef740453ce 100644
--- a/packages/desktop-client/src/components/MobileWebMessage.tsx
+++ b/packages/desktop-client/src/components/MobileWebMessage.tsx
@@ -1,10 +1,6 @@
 import React, { useState } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
-
-import { savePrefs } from 'loot-core/src/client/actions';
-import { type State } from 'loot-core/src/client/state-types';
-import { type PrefsState } from 'loot-core/src/client/state-types/prefs';
 
+import { useLocalPref } from '../hooks/useLocalPref';
 import { useResponsive } from '../ResponsiveProvider';
 import { theme, styles } from '../style';
 
@@ -16,30 +12,24 @@ import { Checkbox } from './forms';
 const buttonStyle = { border: 0, fontSize: 15, padding: '10px 13px' };
 
 export function MobileWebMessage() {
-  const hideMobileMessagePref = useSelector<
-    State,
-    PrefsState['local']['hideMobileMessage']
-  >(state => {
-    return (state.prefs.local && state.prefs.local.hideMobileMessage) || true;
-  });
+  const [hideMobileMessage = true, setHideMobileMessagePref] =
+    useLocalPref('hideMobileMessage');
 
   const { isNarrowWidth } = useResponsive();
 
   const [show, setShow] = useState(
     isNarrowWidth &&
-      !hideMobileMessagePref &&
+      !hideMobileMessage &&
       !document.cookie.match(/hideMobileMessage=true/),
   );
   const [requestDontRemindMe, setRequestDontRemindMe] = useState(false);
 
-  const dispatch = useDispatch();
-
   function onTry() {
     setShow(false);
 
     if (requestDontRemindMe) {
       // remember the pref indefinitely
-      dispatch(savePrefs({ hideMobileMessage: true }));
+      setHideMobileMessagePref(true);
     } else {
       // Set a cookie for 5 minutes
       const d = new Date();
diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx
index 93ae55592185211ae2f9de170903b2678d5fc064..56b599861d5110b833c45198fff611e8534ef2e6 100644
--- a/packages/desktop-client/src/components/Modals.tsx
+++ b/packages/desktop-client/src/components/Modals.tsx
@@ -4,16 +4,10 @@ import { useSelector } from 'react-redux';
 import { useLocation } from 'react-router-dom';
 
 import { type State } from 'loot-core/src/client/state-types';
-import {
-  type ModalsState,
-  type PopModalAction,
-} from 'loot-core/src/client/state-types/modals';
-import { type PrefsState } from 'loot-core/src/client/state-types/prefs';
-import { type QueriesState } from 'loot-core/src/client/state-types/queries';
+import { type PopModalAction } from 'loot-core/src/client/state-types/modals';
 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 { CategoryGroupMenu } from './modals/CategoryGroupMenu';
@@ -56,19 +50,8 @@ export type CommonModalProps = {
 };
 
 export function Modals() {
-  const modalStack = useSelector<State, ModalsState['modalStack']>(
-    state => state.modals.modalStack,
-  );
-  const isHidden = useSelector<State, ModalsState['isHidden']>(
-    state => state.modals.isHidden,
-  );
-  const accounts = useSelector<State, QueriesState['accounts']>(
-    state => state.queries.accounts,
-  );
-  const { grouped: categoryGroups, list: categories } = useCategories();
-  const budgetId = useSelector<State, PrefsState['local']['id']>(
-    state => state.prefs.local && state.prefs.local.id,
-  );
+  const modalStack = useSelector((state: State) => state.modals.modalStack);
+  const isHidden = useSelector((state: State) => state.modals.isHidden);
   const actions = useActions();
   const location = useLocation();
 
@@ -118,8 +101,6 @@ export function Modals() {
               account={options.account}
               balance={options.balance}
               canDelete={options.canDelete}
-              accounts={accounts.filter(acct => acct.closed === 0)}
-              categoryGroups={categoryGroups}
               actions={actions}
             />
           );
@@ -130,7 +111,6 @@ export function Modals() {
               modalProps={modalProps}
               externalAccounts={options.accounts}
               requisitionId={options.requisitionId}
-              localAccounts={accounts.filter(acct => acct.closed === 0)}
               actions={actions}
               syncSource={options.syncSource}
             />
@@ -140,15 +120,8 @@ export function Modals() {
           return (
             <ConfirmCategoryDelete
               modalProps={modalProps}
-              category={
-                'category' in options &&
-                categories.find(c => c.id === options.category)
-              }
-              group={
-                'group' in options &&
-                categoryGroups.find(g => g.id === options.group)
-              }
-              categoryGroups={categoryGroups}
+              category={options.category}
+              group={options.group}
               onDelete={options.onDelete}
             />
           );
@@ -166,7 +139,7 @@ export function Modals() {
           return (
             <LoadBackup
               watchUpdates
-              budgetId={budgetId}
+              budgetId={options.budgetId}
               modalProps={modalProps}
               actions={actions}
               backupDisabled={false}
diff --git a/packages/desktop-client/src/components/Notifications.tsx b/packages/desktop-client/src/components/Notifications.tsx
index ef5db09f4fbbfbd45391c985f453ed19e72d3541..290b3ae7c908b13520ecfdd52ec7fdea6cca5112 100644
--- a/packages/desktop-client/src/components/Notifications.tsx
+++ b/packages/desktop-client/src/components/Notifications.tsx
@@ -7,11 +7,8 @@ import React, {
 } from 'react';
 import { useSelector } from 'react-redux';
 
-import { type State } from 'loot-core/client/state-types';
-import type {
-  NotificationWithId,
-  NotificationsState,
-} from 'loot-core/src/client/state-types/notifications';
+import { type State } from 'loot-core/src/client/state-types';
+import type { NotificationWithId } from 'loot-core/src/client/state-types/notifications';
 
 import { useActions } from '../hooks/useActions';
 import { AnimatedLoading } from '../icons/AnimatedLoading';
@@ -242,8 +239,8 @@ function Notification({
 
 export function Notifications({ style }: { style?: CSSProperties }) {
   const { removeNotification } = useActions();
-  const notifications = useSelector<State, NotificationsState['notifications']>(
-    state => state.notifications.notifications,
+  const notifications = useSelector(
+    (state: State) => state.notifications.notifications,
   );
   return (
     <View
diff --git a/packages/desktop-client/src/components/PrivacyFilter.tsx b/packages/desktop-client/src/components/PrivacyFilter.tsx
index f8596b7c86d5da3be9165c72e2a4addf646fe5f5..5a455dc60dd332c013ed8f1f424e96162a3c9ebd 100644
--- a/packages/desktop-client/src/components/PrivacyFilter.tsx
+++ b/packages/desktop-client/src/components/PrivacyFilter.tsx
@@ -7,8 +7,7 @@ import React, {
   type ReactNode,
 } from 'react';
 
-import { usePrivacyMode } from 'loot-core/src/client/privacy';
-
+import { usePrivacyMode } from '../hooks/usePrivacyMode';
 import { useResponsive } from '../ResponsiveProvider';
 
 import { View } from './common/View';
diff --git a/packages/desktop-client/src/components/ThemeSelector.tsx b/packages/desktop-client/src/components/ThemeSelector.tsx
index e8daef9d686fda3c670495be2390ba00c9789288..415089cb1a456af216db0e486a571b007bf5f96e 100644
--- a/packages/desktop-client/src/components/ThemeSelector.tsx
+++ b/packages/desktop-client/src/components/ThemeSelector.tsx
@@ -2,7 +2,6 @@ import React, { useState } from 'react';
 
 import type { Theme } from 'loot-core/src/types/prefs';
 
-import { useActions } from '../hooks/useActions';
 import { SvgMoonStars, SvgSun, SvgSystem } from '../icons/v2';
 import { useResponsive } from '../ResponsiveProvider';
 import { type CSSProperties, themeOptions, useTheme } from '../style';
@@ -16,8 +15,7 @@ type ThemeSelectorProps = {
 };
 
 export function ThemeSelector({ style }: ThemeSelectorProps) {
-  const theme = useTheme();
-  const { saveGlobalPrefs } = useActions();
+  const [theme, switchTheme] = useTheme();
   const [menuOpen, setMenuOpen] = useState(false);
 
   const { isNarrowWidth } = useResponsive();
@@ -28,12 +26,9 @@ export function ThemeSelector({ style }: ThemeSelectorProps) {
     auto: SvgSystem,
   } as const;
 
-  async function onMenuSelect(newTheme: string) {
+  function onMenuSelect(newTheme: Theme) {
     setMenuOpen(false);
-
-    saveGlobalPrefs({
-      theme: newTheme as Theme,
-    });
+    switchTheme(newTheme);
   }
 
   const Icon = themeIcons[theme] || SvgSun;
diff --git a/packages/desktop-client/src/components/Titlebar.tsx b/packages/desktop-client/src/components/Titlebar.tsx
index b532412dc29e8bd321610b2728939511f8e1f910..8a08b283f350e3b60550b4a66176ef6413426389 100644
--- a/packages/desktop-client/src/components/Titlebar.tsx
+++ b/packages/desktop-client/src/components/Titlebar.tsx
@@ -6,11 +6,8 @@ import React, {
   useContext,
   type ReactNode,
 } from 'react';
-import { useSelector } from 'react-redux';
 import { Routes, Route, useLocation } from 'react-router-dom';
 
-import { type State } from 'loot-core/client/state-types';
-import { type PrefsState } from 'loot-core/client/state-types/prefs';
 import * as Platform from 'loot-core/src/client/platform';
 import * as queries from 'loot-core/src/client/queries';
 import { listen } from 'loot-core/src/platform/client/fetch';
@@ -18,6 +15,8 @@ import { type LocalPrefs } from 'loot-core/src/types/prefs';
 
 import { useActions } from '../hooks/useActions';
 import { useFeatureFlag } from '../hooks/useFeatureFlag';
+import { useGlobalPref } from '../hooks/useGlobalPref';
+import { useLocalPref } from '../hooks/useLocalPref';
 import { useNavigate } from '../hooks/useNavigate';
 import { SvgArrowLeft } from '../icons/v1';
 import {
@@ -41,7 +40,7 @@ import { View } from './common/View';
 import { KeyHandlers } from './KeyHandlers';
 import { LoggedInUser } from './LoggedInUser';
 import { useServerURL } from './ServerContext';
-import { useSidebar } from './sidebar';
+import { useSidebar } from './sidebar/SidebarProvider';
 import { useSheetValue } from './spreadsheet/useSheetValue';
 import { ThemeSelector } from './ThemeSelector';
 import { Tooltip } from './tooltips';
@@ -120,11 +119,8 @@ type PrivacyButtonProps = {
 };
 
 function PrivacyButton({ style }: PrivacyButtonProps) {
-  const isPrivacyEnabled = useSelector<
-    State,
-    PrefsState['local']['isPrivacyEnabled']
-  >(state => state.prefs.local?.isPrivacyEnabled);
-  const { savePrefs } = useActions();
+  const [isPrivacyEnabled, setPrivacyEnabledPref] =
+    useLocalPref('isPrivacyEnabled');
 
   const privacyIconStyle = { width: 15, height: 15 };
 
@@ -132,7 +128,7 @@ function PrivacyButton({ style }: PrivacyButtonProps) {
     <Button
       type="bare"
       aria-label={`${isPrivacyEnabled ? 'Disable' : 'Enable'} privacy mode`}
-      onClick={() => savePrefs({ isPrivacyEnabled: !isPrivacyEnabled })}
+      onClick={() => setPrivacyEnabledPref(!isPrivacyEnabled)}
       style={style}
     >
       {isPrivacyEnabled ? (
@@ -149,9 +145,7 @@ type SyncButtonProps = {
   isMobile?: boolean;
 };
 function SyncButton({ style, isMobile = false }: SyncButtonProps) {
-  const cloudFileId = useSelector<State, PrefsState['local']['cloudFileId']>(
-    state => state.prefs.local?.cloudFileId,
-  );
+  const [cloudFileId] = useLocalPref('cloudFileId');
   const { sync } = useActions();
 
   const [syncing, setSyncing] = useState(false);
@@ -291,13 +285,8 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) {
 }
 
 function BudgetTitlebar() {
-  const maxMonths = useSelector<State, PrefsState['global']['maxMonths']>(
-    state => state.prefs.global?.maxMonths,
-  );
-  const budgetType = useSelector<State, PrefsState['local']['budgetType']>(
-    state => state.prefs.local?.budgetType,
-  );
-  const { saveGlobalPrefs } = useActions();
+  const [maxMonths, setMaxMonthsPref] = useGlobalPref('maxMonths');
+  const [budgetType] = useLocalPref('budgetType');
   const { sendEvent } = useContext(TitlebarContext);
 
   const [loading, setLoading] = useState(false);
@@ -326,7 +315,7 @@ function BudgetTitlebar() {
     <View style={{ flexDirection: 'row', alignItems: 'center' }}>
       <MonthCountSelector
         maxMonths={maxMonths || 1}
-        onChange={value => saveGlobalPrefs({ maxMonths: value })}
+        onChange={value => setMaxMonthsPref(value)}
       />
       {reportBudgetEnabled && (
         <View style={{ marginLeft: -5 }}>
@@ -399,10 +388,7 @@ export function Titlebar({ style }: TitlebarProps) {
   const sidebar = useSidebar();
   const { isNarrowWidth } = useResponsive();
   const serverURL = useServerURL();
-  const floatingSidebar = useSelector<
-    State,
-    PrefsState['global']['floatingSidebar']
-  >(state => state.prefs.global?.floatingSidebar);
+  const [floatingSidebar] = useGlobalPref('floatingSidebar');
 
   return isNarrowWidth ? null : (
     <View
diff --git a/packages/desktop-client/src/components/UpdateNotification.tsx b/packages/desktop-client/src/components/UpdateNotification.tsx
index 18c9924662e7f1846e84919beee00504b2146684..f7052502880af3f4a6f5d0be50c2fae25a98f82b 100644
--- a/packages/desktop-client/src/components/UpdateNotification.tsx
+++ b/packages/desktop-client/src/components/UpdateNotification.tsx
@@ -1,8 +1,7 @@
 import React from 'react';
 import { useSelector } from 'react-redux';
 
-import { type State } from 'loot-core/client/state-types';
-import { type AppState } from 'loot-core/client/state-types/app';
+import { type State } from 'loot-core/src/client/state-types';
 
 import { useActions } from '../hooks/useActions';
 import { SvgClose } from '../icons/v1';
@@ -14,13 +13,10 @@ import { Text } from './common/Text';
 import { View } from './common/View';
 
 export function UpdateNotification() {
-  const updateInfo = useSelector<State, AppState['updateInfo']>(
-    state => state.app.updateInfo,
+  const updateInfo = useSelector((state: State) => state.app.updateInfo);
+  const showUpdateNotification = useSelector(
+    (state: State) => state.app.showUpdateNotification,
   );
-  const showUpdateNotification = useSelector<
-    State,
-    AppState['showUpdateNotification']
-  >(state => state.app.showUpdateNotification);
 
   const { updateApp, setAppState } = useActions();
 
diff --git a/packages/desktop-client/src/components/accounts/Account.jsx b/packages/desktop-client/src/components/accounts/Account.jsx
index 1fb7e32a11619d83ac1fc93446cf90731a2085d1..48f30a99a4490c7de247f868a0f8dcacc8830a4b 100644
--- a/packages/desktop-client/src/components/accounts/Account.jsx
+++ b/packages/desktop-client/src/components/accounts/Account.jsx
@@ -26,7 +26,12 @@ import {
 } from 'loot-core/src/shared/transactions';
 import { applyChanges, groupById } from 'loot-core/src/shared/util';
 
+import { useAccounts } from '../../hooks/useAccounts';
 import { useCategories } from '../../hooks/useCategories';
+import { useDateFormat } from '../../hooks/useDateFormat';
+import { useFailedAccounts } from '../../hooks/useFailedAccounts';
+import { useLocalPref } from '../../hooks/useLocalPref';
+import { usePayees } from '../../hooks/usePayees';
 import { SelectedProviderWithItems } from '../../hooks/useSelected';
 import { styles, theme } from '../../style';
 import { Button } from '../common/Button';
@@ -1532,23 +1537,41 @@ export function Account() {
   const location = useLocation();
 
   const { grouped: categoryGroups } = useCategories();
-  const state = useSelector(state => ({
-    newTransactions: state.queries.newTransactions,
-    matchedTransactions: state.queries.matchedTransactions,
-    accounts: state.queries.accounts,
-    failedAccounts: state.account.failedAccounts,
-    dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy',
-    hideFraction: state.prefs.local.hideFraction || false,
-    expandSplits: state.prefs.local['expand-splits'],
-    showBalances: params.id && state.prefs.local['show-balances-' + params.id],
-    showCleared: params.id && !state.prefs.local['hide-cleared-' + params.id],
-    showExtraBalances:
-      state.prefs.local['show-extra-balances-' + params.id || 'all-accounts'],
-    payees: state.queries.payees,
-    modalShowing: state.modals.modalStack.length > 0,
-    accountsSyncing: state.account.accountsSyncing,
-    lastUndoState: state.app.lastUndoState,
-  }));
+  const newTransactions = useSelector(state => state.queries.newTransactions);
+  const matchedTransactions = useSelector(
+    state => state.queries.matchedTransactions,
+  );
+  const accounts = useAccounts();
+  const payees = usePayees();
+  const failedAccounts = useFailedAccounts();
+  const dateFormat = useDateFormat() || 'MM/dd/yyyy';
+  const [hideFraction = false] = useLocalPref('hideFraction');
+  const [expandSplits] = useLocalPref('expand-splits');
+  const [showBalances] = useLocalPref(`show-balances-${params.id}`);
+  const [hideCleared] = useLocalPref(`hide-cleared-${params.id}`);
+  const [showExtraBalances] = useLocalPref(
+    `show-extra-balances-${params.id || 'all-accounts'}`,
+  );
+  const modalShowing = useSelector(state => state.modals.modalStack.length > 0);
+  const accountsSyncing = useSelector(state => state.account.accountsSyncing);
+  const lastUndoState = useSelector(state => state.app.lastUndoState);
+
+  const state = {
+    newTransactions,
+    matchedTransactions,
+    accounts,
+    failedAccounts,
+    dateFormat,
+    hideFraction,
+    expandSplits,
+    showBalances,
+    showCleared: !hideCleared,
+    showExtraBalances,
+    payees,
+    modalShowing,
+    accountsSyncing,
+    lastUndoState,
+  };
 
   const dispatch = useDispatch();
   const filtersList = useFilters();
diff --git a/packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx b/packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx
index 423c99b7c73006d27b3b7812ad4f26743d40e4ec..09180381df0d24b5c7b256505c014b3b5c4b9cf2 100644
--- a/packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx
+++ b/packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx
@@ -3,6 +3,7 @@ import { useSelector } from 'react-redux';
 import { useParams } from 'react-router-dom';
 
 import { authorizeBank } from '../../gocardless';
+import { useAccounts } from '../../hooks/useAccounts';
 import { useActions } from '../../hooks/useActions';
 import { SvgExclamationOutline } from '../../icons/v1';
 import { theme } from '../../style';
@@ -49,7 +50,7 @@ function getErrorMessage(type, code) {
 }
 
 export function AccountSyncCheck() {
-  const accounts = useSelector(state => state.queries.accounts);
+  const accounts = useAccounts();
   const failedAccounts = useSelector(state => state.account.failedAccounts);
   const { unlinkAccount, pushModal } = useActions();
 
diff --git a/packages/desktop-client/src/components/accounts/Header.jsx b/packages/desktop-client/src/components/accounts/Header.jsx
index 4e181a5146c4449d06e0a24a8302793e367c45fd..80c1030ca219e0148c0762036a18a5a24de02c68 100644
--- a/packages/desktop-client/src/components/accounts/Header.jsx
+++ b/packages/desktop-client/src/components/accounts/Header.jsx
@@ -1,5 +1,6 @@
 import React, { useState, useRef } from 'react';
 
+import { useLocalPref } from '../../hooks/useLocalPref';
 import { useSyncServerStatus } from '../../hooks/useSyncServerStatus';
 import { AnimatedLoading } from '../../icons/AnimatedLoading';
 import { SvgAdd } from '../../icons/v1';
@@ -53,7 +54,6 @@ export function AccountHeader({
   search,
   filters,
   conditionsOp,
-  savePrefs,
   pushModal,
   onSearch,
   onAddTransaction,
@@ -86,6 +86,7 @@ export function AccountHeader({
   const syncServerStatus = useSyncServerStatus();
   const isUsingServer = syncServerStatus !== 'no-server';
   const isServerOffline = syncServerStatus === 'offline';
+  const [_, setExpandSplitsPref] = useLocalPref('expand-splits');
 
   let canSync = account && account.account_id && isUsingServer;
   if (!account) {
@@ -100,9 +101,7 @@ export function AccountHeader({
         id: tableRef.current.getScrolledItem(),
       });
 
-      savePrefs({
-        'expand-splits': !(splitsExpanded.state.mode === 'expand'),
-      });
+      setExpandSplitsPref(!(splitsExpanded.state.mode === 'expand'));
     }
   }
 
diff --git a/packages/desktop-client/src/components/accounts/MobileAccount.jsx b/packages/desktop-client/src/components/accounts/MobileAccount.jsx
index b8d4bb6ce772263468d6e385c6217c6b889e1288..0b06e280e08fe92f3a69620660a239a40f5b0348 100644
--- a/packages/desktop-client/src/components/accounts/MobileAccount.jsx
+++ b/packages/desktop-client/src/components/accounts/MobileAccount.jsx
@@ -19,8 +19,13 @@ import {
   ungroupTransactions,
 } from 'loot-core/src/shared/transactions';
 
+import { useAccounts } from '../../hooks/useAccounts';
 import { useCategories } from '../../hooks/useCategories';
+import { useDateFormat } from '../../hooks/useDateFormat';
+import { useLocalPref } from '../../hooks/useLocalPref';
+import { useLocalPrefs } from '../../hooks/useLocalPrefs';
 import { useNavigate } from '../../hooks/useNavigate';
+import { usePayees } from '../../hooks/usePayees';
 import { useSetThemeColor } from '../../hooks/useSetThemeColor';
 import { theme, styles } from '../../style';
 import { Button } from '../common/Button';
@@ -72,19 +77,27 @@ function PreviewTransactions({ children }) {
 let paged;
 
 export function Account(props) {
-  const accounts = useSelector(state => state.queries.accounts);
+  const accounts = useAccounts();
+  const payees = usePayees();
 
   const navigate = useNavigate();
   const [transactions, setTransactions] = useState([]);
   const [searchText, setSearchText] = useState('');
   const [currentQuery, setCurrentQuery] = useState();
 
-  const state = useSelector(state => ({
-    payees: state.queries.payees,
-    newTransactions: state.queries.newTransactions,
-    prefs: state.prefs.local,
-    dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy',
-  }));
+  const newTransactions = useSelector(state => state.queries.newTransactions);
+  const prefs = useLocalPrefs();
+  const dateFormat = useDateFormat() || 'MM/dd/yyyy';
+  const [_numberFormat] = useLocalPref('numberFormat');
+  const numberFormat = _numberFormat || 'comma-dot';
+  const [hideFraction = false] = useLocalPref('hideFraction');
+
+  const state = {
+    payees,
+    newTransactions,
+    prefs,
+    dateFormat,
+  };
 
   const dispatch = useDispatch();
   const actionCreators = useMemo(
@@ -134,11 +147,6 @@ export function Account(props) {
         }
       });
 
-      if (accounts.length === 0) {
-        await actionCreators.getAccounts();
-      }
-
-      await actionCreators.initiallyLoadPayees();
       await fetchTransactions();
 
       actionCreators.markAccountRead(accountId);
@@ -216,8 +224,6 @@ export function Account(props) {
   const balance = queries.accountBalance(account);
   const balanceCleared = queries.accountBalanceCleared(account);
   const balanceUncleared = queries.accountBalanceUncleared(account);
-  const numberFormat = state.prefs.numberFormat || 'comma-dot';
-  const hideFraction = state.prefs.hideFraction || false;
 
   return (
     <SchedulesProvider
diff --git a/packages/desktop-client/src/components/accounts/MobileAccounts.jsx b/packages/desktop-client/src/components/accounts/MobileAccounts.jsx
index 4087fdbe0d03ddc65ae71a94df511f3de20ba94a..f056629501eb2d3345e269edb3be9a9b88c4f19d 100644
--- a/packages/desktop-client/src/components/accounts/MobileAccounts.jsx
+++ b/packages/desktop-client/src/components/accounts/MobileAccounts.jsx
@@ -1,10 +1,12 @@
-import React, { useEffect, useState } from 'react';
-import { useSelector } from 'react-redux';
+import React, { useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
 
+import { replaceModal, syncAndDownload } from 'loot-core/src/client/actions';
 import * as queries from 'loot-core/src/client/queries';
 
-import { useActions } from '../../hooks/useActions';
+import { useAccounts } from '../../hooks/useAccounts';
 import { useCategories } from '../../hooks/useCategories';
+import { useLocalPref } from '../../hooks/useLocalPref';
 import { useNavigate } from '../../hooks/useNavigate';
 import { useSetThemeColor } from '../../hooks/useSetThemeColor';
 import { SvgAdd } from '../../icons/v1';
@@ -221,26 +223,19 @@ function AccountList({
 }
 
 export function Accounts() {
-  const accounts = useSelector(state => state.queries.accounts);
+  const dispatch = useDispatch();
+  const accounts = useAccounts();
   const newTransactions = useSelector(state => state.queries.newTransactions);
   const updatedAccounts = useSelector(state => state.queries.updatedAccounts);
-  const numberFormat = useSelector(
-    state => state.prefs.local.numberFormat || 'comma-dot',
-  );
-  const hideFraction = useSelector(
-    state => state.prefs.local.hideFraction || false,
-  );
+  const [_numberFormat] = useLocalPref('numberFormat');
+  const numberFormat = _numberFormat || 'comma-dot';
+  const [hideFraction = false] = useLocalPref('hideFraction');
 
   const { list: categories } = useCategories();
-  const { getAccounts, replaceModal, syncAndDownload } = useActions();
 
   const transactions = useState({});
   const navigate = useNavigate();
 
-  useEffect(() => {
-    (async () => getAccounts())();
-  }, []);
-
   const onSelectAccount = id => {
     navigate(`/accounts/${id}`);
   };
@@ -249,6 +244,14 @@ export function Accounts() {
     navigate(`/transaction/${transaction}`);
   };
 
+  const onAddAccount = () => {
+    dispatch(replaceModal('add-account'));
+  };
+
+  const onSync = () => {
+    dispatch(syncAndDownload());
+  };
+
   useSetThemeColor(theme.mobileViewTheme);
 
   return (
@@ -265,10 +268,10 @@ export function Accounts() {
         getBalanceQuery={queries.accountBalance}
         getOnBudgetBalance={queries.budgetedAccountBalance}
         getOffBudgetBalance={queries.offbudgetAccountBalance}
-        onAddAccount={() => replaceModal('add-account')}
+        onAddAccount={onAddAccount}
         onSelectAccount={onSelectAccount}
         onSelectTransaction={onSelectTransaction}
-        onSync={syncAndDownload}
+        onSync={onSync}
       />
     </View>
   );
diff --git a/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx
index f296e7b297fdafd9e25b3d6c809b2b54dee96198..778480a7dff82db286cef18c2cdcdeceb399e3c8 100644
--- a/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx
+++ b/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx
@@ -3,9 +3,9 @@ import React, { Fragment, type ComponentProps, type ReactNode } from 'react';
 
 import { css } from 'glamor';
 
-import { useCachedAccounts } from 'loot-core/src/client/data-hooks/accounts';
 import { type AccountEntity } from 'loot-core/src/types/models';
 
+import { useAccounts } from '../../hooks/useAccounts';
 import { useResponsive } from '../../ResponsiveProvider';
 import { type CSSProperties, theme } from '../../style';
 import { View } from '../common/View';
@@ -86,7 +86,7 @@ export function AccountAutocomplete({
   closeOnBlur,
   ...props
 }: AccountAutoCompleteProps) {
-  let accounts = useCachedAccounts() || [];
+  let accounts = useAccounts() || [];
 
   //remove closed accounts if needed
   //then sort by closed, then offbudget
diff --git a/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx
index b99009b32270be84972f6fb896106af779fa5ed1..84ee34bfd79d3967db97247d4037d7ef4fa3c7ad 100644
--- a/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx
+++ b/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx
@@ -13,14 +13,14 @@ import { useDispatch } from 'react-redux';
 import { css } from 'glamor';
 
 import { createPayee } from 'loot-core/src/client/actions/queries';
-import { useCachedAccounts } from 'loot-core/src/client/data-hooks/accounts';
-import { useCachedPayees } from 'loot-core/src/client/data-hooks/payees';
 import { getActivePayees } from 'loot-core/src/client/reducers/queries';
 import {
   type AccountEntity,
   type PayeeEntity,
 } from 'loot-core/src/types/models';
 
+import { useAccounts } from '../../hooks/useAccounts';
+import { usePayees } from '../../hooks/usePayees';
 import { SvgAdd } from '../../icons/v1';
 import { useResponsive } from '../../ResponsiveProvider';
 import { type CSSProperties, theme } from '../../style';
@@ -187,12 +187,12 @@ export function PayeeAutocomplete({
   payees,
   ...props
 }: PayeeAutocompleteProps) {
-  const cachedPayees = useCachedPayees();
+  const retrievedPayees = usePayees();
   if (!payees) {
-    payees = cachedPayees;
+    payees = retrievedPayees;
   }
 
-  const cachedAccounts = useCachedAccounts();
+  const cachedAccounts = useAccounts();
   if (!accounts) {
     accounts = cachedAccounts;
   }
diff --git a/packages/desktop-client/src/components/budget/BudgetCategories.jsx b/packages/desktop-client/src/components/budget/BudgetCategories.jsx
index c80bfb2ab354aaef372b4766ee7aea22f2daa550..51183003fcd1f0171b49ae546f02f8e81744a6c3 100644
--- a/packages/desktop-client/src/components/budget/BudgetCategories.jsx
+++ b/packages/desktop-client/src/components/budget/BudgetCategories.jsx
@@ -1,5 +1,6 @@
 import React, { memo, useState, useMemo } from 'react';
 
+import { useLocalPref } from '../../hooks/useLocalPref';
 import { theme, styles } from '../../style';
 import { View } from '../common/View';
 import { DropHighlightPosContext } from '../sort';
@@ -17,12 +18,7 @@ import { separateGroups } from './util';
 export const BudgetCategories = memo(
   ({
     categoryGroups,
-    newCategoryForGroup,
-    showHiddenCategories,
-    isAddingGroup,
     editingCell,
-    collapsed,
-    setCollapsed,
     dataComponents,
     onBudgetAction,
     onShowActivity,
@@ -34,11 +30,16 @@ export const BudgetCategories = memo(
     onDeleteGroup,
     onReorderCategory,
     onReorderGroup,
-    onShowNewCategory,
-    onHideNewCategory,
-    onShowNewGroup,
-    onHideNewGroup,
   }) => {
+    const [_collapsed, setCollapsedPref] = useLocalPref('budget.collapsed');
+    const collapsed = _collapsed || [];
+    const [showHiddenCategories] = useLocalPref('budget.showHiddenCategories');
+    function onCollapse(value) {
+      setCollapsedPref(value);
+    }
+
+    const [isAddingGroup, setIsAddingGroup] = useState(false);
+    const [newCategoryForGroup, setNewCategoryForGroup] = useState(null);
     const items = useMemo(() => {
       const [expenseGroups, incomeGroup] = separateGroups(categoryGroups);
 
@@ -133,15 +134,46 @@ export const BudgetCategories = memo(
         });
       } else if (state === 'end') {
         setDragState(null);
-        setCollapsed(savedCollapsed || []);
+        onCollapse(savedCollapsed || []);
       }
     }
 
     function onToggleCollapse(id) {
       if (collapsed.includes(id)) {
-        setCollapsed(collapsed.filter(id_ => id_ !== id));
+        onCollapse(collapsed.filter(id_ => id_ !== id));
       } else {
-        setCollapsed([...collapsed, id]);
+        onCollapse([...collapsed, id]);
+      }
+    }
+
+    function onShowNewGroup() {
+      setIsAddingGroup(true);
+    }
+
+    function onHideNewGroup() {
+      setIsAddingGroup(false);
+    }
+
+    function _onSaveGroup(group) {
+      onSaveGroup?.(group);
+      if (group.id === 'new') {
+        onHideNewGroup();
+      }
+    }
+
+    function onShowNewCategory(groupId) {
+      onCollapse(collapsed.filter(c => c !== groupId));
+      setNewCategoryForGroup(groupId);
+    }
+
+    function onHideNewCategory() {
+      setNewCategoryForGroup(null);
+    }
+
+    function _onSaveCategory(category) {
+      onSaveCategory?.(category);
+      if (category.id === 'new') {
+        onHideNewCategory();
       }
     }
 
@@ -167,7 +199,7 @@ export const BudgetCategories = memo(
                   <SidebarGroup
                     group={{ id: 'new', name: '' }}
                     editing={true}
-                    onSave={onSaveGroup}
+                    onSave={_onSaveGroup}
                     onHideNewGroup={onHideNewGroup}
                     onEdit={onEditName}
                   />
@@ -187,7 +219,7 @@ export const BudgetCategories = memo(
                       id: 'new',
                     }}
                     editing={true}
-                    onSave={onSaveCategory}
+                    onSave={_onSaveCategory}
                     onHideNewCategory={onHideNewCategory}
                     onEditName={onEditName}
                   />
@@ -204,7 +236,7 @@ export const BudgetCategories = memo(
                   MonthComponent={dataComponents.ExpenseGroupComponent}
                   dragState={dragState}
                   onEditName={onEditName}
-                  onSave={onSaveGroup}
+                  onSave={_onSaveGroup}
                   onDelete={onDeleteGroup}
                   onDragChange={onDragChange}
                   onReorderGroup={onReorderGroup}
@@ -223,7 +255,7 @@ export const BudgetCategories = memo(
                   dragState={dragState}
                   onEditName={onEditName}
                   onEditMonth={onEditMonth}
-                  onSave={onSaveCategory}
+                  onSave={_onSaveCategory}
                   onDelete={onDeleteCategory}
                   onDragChange={onDragChange}
                   onReorder={onReorderCategory}
@@ -255,7 +287,7 @@ export const BudgetCategories = memo(
                   MonthComponent={dataComponents.IncomeGroupComponent}
                   collapsed={collapsed.includes(item.value.id)}
                   onEditName={onEditName}
-                  onSave={onSaveGroup}
+                  onSave={_onSaveGroup}
                   onToggleCollapse={onToggleCollapse}
                   onShowNewCategory={onShowNewCategory}
                 />
@@ -270,7 +302,7 @@ export const BudgetCategories = memo(
                   MonthComponent={dataComponents.IncomeCategoryComponent}
                   onEditName={onEditName}
                   onEditMonth={onEditMonth}
-                  onSave={onSaveCategory}
+                  onSave={_onSaveCategory}
                   onDelete={onDeleteCategory}
                   onDragChange={onDragChange}
                   onReorder={onReorderCategory}
diff --git a/packages/desktop-client/src/components/budget/BudgetTable.jsx b/packages/desktop-client/src/components/budget/BudgetTable.jsx
index 3000ebb1af4e1c161f7c555291d410ef94b08f37..d9f4d4df8bb8a7b93fcf192471b274575d3c20f6 100644
--- a/packages/desktop-client/src/components/budget/BudgetTable.jsx
+++ b/packages/desktop-client/src/components/budget/BudgetTable.jsx
@@ -1,5 +1,7 @@
 import React, { createRef, Component } from 'react';
+import { connect } from 'react-redux';
 
+import { savePrefs } from 'loot-core/src/client/actions';
 import * as monthUtils from 'loot-core/src/shared/months';
 
 import { theme, styles } from '../../style';
@@ -12,7 +14,7 @@ import { BudgetTotals } from './BudgetTotals';
 import { MonthsProvider } from './MonthsContext';
 import { findSortDown, findSortUp, getScrollbarWidth } from './util';
 
-export class BudgetTable extends Component {
+class BudgetTableInner extends Component {
   constructor(props) {
     super(props);
     this.budgetCategoriesRef = createRef();
@@ -20,7 +22,6 @@ export class BudgetTable extends Component {
     this.state = {
       editing: null,
       draggingState: null,
-      showHiddenCategories: props.prefs['budget.showHiddenCategories'] ?? false,
     };
   }
 
@@ -137,26 +138,22 @@ export class BudgetTable extends Component {
     return monthUtils.addMonths(this.props.startMonth, monthIndex);
   };
 
+  // This is called via ref.
   clearEditing() {
     this.setState({ editing: null });
   }
 
   toggleHiddenCategories = () => {
-    this.setState(prevState => ({
-      showHiddenCategories: !prevState.showHiddenCategories,
-    }));
-    this.props.savePrefs({
-      'budget.showHiddenCategories': !this.state.showHiddenCategories,
-    });
+    this.props.onToggleHiddenCategories();
   };
 
   expandAllCategories = () => {
-    this.props.setCollapsed([]);
+    this.props.onCollapse([]);
   };
 
   collapseAllCategories = () => {
-    const { setCollapsed, categoryGroups } = this.props;
-    setCollapsed(categoryGroups.map(g => g.id));
+    const { onCollapse, categoryGroups } = this.props;
+    onCollapse(categoryGroups.map(g => g.id));
   };
 
   render() {
@@ -167,21 +164,13 @@ export class BudgetTable extends Component {
       startMonth,
       numMonths,
       monthBounds,
-      collapsed,
-      setCollapsed,
-      newCategoryForGroup,
       dataComponents,
-      isAddingGroup,
       onSaveCategory,
       onSaveGroup,
       onDeleteCategory,
       onDeleteGroup,
-      onShowNewCategory,
-      onHideNewCategory,
-      onShowNewGroup,
-      onHideNewGroup,
     } = this.props;
-    const { editing, draggingState, showHiddenCategories } = this.state;
+    const { editing, draggingState } = this.state;
 
     return (
       <View
@@ -254,13 +243,8 @@ export class BudgetTable extends Component {
                 innerRef={el => (this.budgetDataNode = el)}
               >
                 <BudgetCategories
-                  showHiddenCategories={showHiddenCategories}
                   categoryGroups={categoryGroups}
-                  newCategoryForGroup={newCategoryForGroup}
-                  isAddingGroup={isAddingGroup}
                   editingCell={editing}
-                  collapsed={collapsed}
-                  setCollapsed={setCollapsed}
                   dataComponents={dataComponents}
                   onEditMonth={this.onEditMonth}
                   onEditName={this.onEditName}
@@ -270,10 +254,6 @@ export class BudgetTable extends Component {
                   onDeleteGroup={onDeleteGroup}
                   onReorderCategory={this.onReorderCategory}
                   onReorderGroup={this.onReorderGroup}
-                  onShowNewCategory={onShowNewCategory}
-                  onHideNewCategory={onHideNewCategory}
-                  onShowNewGroup={onShowNewGroup}
-                  onHideNewGroup={onHideNewGroup}
                   onBudgetAction={this.onBudgetAction}
                   onShowActivity={this.onShowActivity}
                 />
@@ -285,3 +265,35 @@ export class BudgetTable extends Component {
     );
   }
 }
+
+const mapStateToProps = state => {
+  const { grouped: categoryGroups } = state.queries.categories;
+  return {
+    categoryGroups,
+  };
+};
+
+const mapDispatchToProps = dispatch => {
+  const onCollapse = collapsedIds => {
+    dispatch(savePrefs({ 'budget.collapsed': collapsedIds }));
+  };
+
+  const onToggleHiddenCategories = () =>
+    dispatch((innerDispatch, getState) => {
+      const { prefs } = getState();
+      const showHiddenCategories = prefs.local['budget.showHiddenCategories'];
+      innerDispatch(
+        savePrefs({
+          'budget.showHiddenCategories': !showHiddenCategories,
+        }),
+      );
+    });
+  return {
+    onCollapse,
+    onToggleHiddenCategories,
+  };
+};
+
+export const BudgetTable = connect(mapStateToProps, mapDispatchToProps, null, {
+  forwardRef: true,
+})(BudgetTableInner);
diff --git a/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx b/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx
index 7a01439ab81c890079171c4429536562a41dbfa2..ab366f3a6087cbe2f4a1ef3d3c8a133b6d002bd7 100644
--- a/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx
+++ b/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx
@@ -1,11 +1,7 @@
 // @ts-strict-ignore
 import React, { forwardRef, useEffect, type ComponentProps } from 'react';
-import { useSelector } from 'react-redux';
 import AutoSizer from 'react-virtualized-auto-sizer';
 
-import { type State } from 'loot-core/client/state-types';
-import { type PrefsState } from 'loot-core/client/state-types/prefs';
-
 import { useActions } from '../../hooks/useActions';
 import { View } from '../common/View';
 
@@ -37,14 +33,13 @@ type DynamicBudgetTableInnerProps = {
 } & ComponentProps<typeof BudgetTable>;
 
 const DynamicBudgetTableInner = forwardRef<
-  BudgetTable,
+  typeof BudgetTable,
   DynamicBudgetTableInnerProps
 >(
   (
     {
       width,
       height,
-      categoryGroups,
       prewarmStartMonth,
       startMonth,
       maxMonths = 3,
@@ -55,9 +50,6 @@ const DynamicBudgetTableInner = forwardRef<
     },
     ref,
   ) => {
-    const prefs = useSelector<State, PrefsState['local']>(
-      state => state.prefs.local,
-    );
     const { setDisplayMax } = useBudgetMonthCount();
     const actions = useActions();
 
@@ -91,12 +83,10 @@ const DynamicBudgetTableInner = forwardRef<
           />
           <BudgetTable
             ref={ref}
-            categoryGroups={categoryGroups}
             prewarmStartMonth={prewarmStartMonth}
             startMonth={startMonth}
             numMonths={numMonths}
             monthBounds={monthBounds}
-            prefs={prefs}
             {...actions}
             {...props}
           />
@@ -107,7 +97,7 @@ const DynamicBudgetTableInner = forwardRef<
 );
 
 export const DynamicBudgetTable = forwardRef<
-  BudgetTable,
+  typeof BudgetTable,
   DynamicBudgetTableInnerProps
 >((props, ref) => {
   return (
diff --git a/packages/desktop-client/src/components/budget/MobileBudget.tsx b/packages/desktop-client/src/components/budget/MobileBudget.tsx
index 4b98fc5f6ea9e232ff6d24b63f9e0b76f7017c5c..f5145aeec43312b06a2e428b5a8b1349a809a834 100644
--- a/packages/desktop-client/src/components/budget/MobileBudget.tsx
+++ b/packages/desktop-client/src/components/budget/MobileBudget.tsx
@@ -1,10 +1,7 @@
 // @ts-strict-ignore
 import React, { useEffect, useState } from 'react';
-import { useSelector } from 'react-redux';
 
-import { type State } from 'loot-core/client/state-types';
 import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider';
-import { type PrefsState } from 'loot-core/src/client/state-types/prefs';
 import { send, listen } from 'loot-core/src/platform/client/fetch';
 import * as monthUtils from 'loot-core/src/shared/months';
 import {
@@ -14,6 +11,7 @@ import {
 
 import { type BoundActions, useActions } from '../../hooks/useActions';
 import { useCategories } from '../../hooks/useCategories';
+import { useLocalPref } from '../../hooks/useLocalPref';
 import { useSetThemeColor } from '../../hooks/useSetThemeColor';
 import { AnimatedLoading } from '../../icons/AnimatedLoading';
 import { theme } from '../../style';
@@ -26,7 +24,6 @@ import { prewarmMonth, switchBudgetType } from './util';
 type BudgetInnerProps = {
   categories: CategoryEntity[];
   categoryGroups: CategoryGroupEntity[];
-  prefs: PrefsState['local'];
   loadPrefs: BoundActions['loadPrefs'];
   savePrefs: BoundActions['savePrefs'];
   budgetType: 'rollover' | 'report';
@@ -50,9 +47,7 @@ function BudgetInner(props: BudgetInnerProps) {
   const {
     categoryGroups,
     categories,
-    prefs,
     loadPrefs,
-    savePrefs,
     budgetType,
     spreadsheet,
     applyBudgetAction,
@@ -75,6 +70,10 @@ function BudgetInner(props: BudgetInnerProps) {
   const [initialized, setInitialized] = useState(false);
   const [editMode, setEditMode] = useState(false);
 
+  const [_numberFormat] = useLocalPref('numberFormat');
+  const numberFormat = _numberFormat || 'comma-dot';
+  const [hideFraction = false] = useLocalPref('hideFraction');
+
   useEffect(() => {
     async function init() {
       const { start, end } = await send('get-budget-bounds');
@@ -356,9 +355,6 @@ function BudgetInner(props: BudgetInnerProps) {
     });
   };
 
-  const numberFormat = prefs?.numberFormat || 'comma-dot';
-  const hideFraction = prefs?.hideFraction || false;
-
   if (!categoryGroups || !initialized) {
     return (
       <View
@@ -385,7 +381,7 @@ function BudgetInner(props: BudgetInnerProps) {
         <BudgetTable
           // This key forces the whole table rerender when the number
           // format changes
-          key={numberFormat + hideFraction}
+          key={`${numberFormat}${hideFraction}`}
           categoryGroups={categoryGroups}
           type={budgetType}
           month={currentMonth}
@@ -407,7 +403,6 @@ function BudgetInner(props: BudgetInnerProps) {
           onBudgetAction={applyBudgetAction}
           onRefresh={onRefresh}
           onSwitchBudgetType={onSwitchBudgetType}
-          savePrefs={savePrefs}
           pushModal={pushModal}
           onEditGroup={onEditGroup}
           onEditCategory={onEditCategory}
@@ -419,12 +414,8 @@ function BudgetInner(props: BudgetInnerProps) {
 
 export function Budget() {
   const { list: categories, grouped: categoryGroups } = useCategories();
-  const budgetType = useSelector<State, PrefsState['local']['budgetType']>(
-    state => state.prefs.local?.budgetType || 'rollover',
-  );
-  const prefs = useSelector<State, PrefsState['local']>(
-    state => state.prefs.local,
-  );
+  const [_budgetType] = useLocalPref('budgetType');
+  const budgetType = _budgetType || 'rollover';
 
   const actions = useActions();
   const spreadsheet = useSpreadsheet();
@@ -434,7 +425,6 @@ export function Budget() {
       categoryGroups={categoryGroups}
       categories={categories}
       budgetType={budgetType}
-      prefs={prefs}
       {...actions}
       spreadsheet={spreadsheet}
     />
diff --git a/packages/desktop-client/src/components/budget/MobileBudgetTable.jsx b/packages/desktop-client/src/components/budget/MobileBudgetTable.jsx
index a7214cf1214efd244ce8c15ae9056a0b0ec9bd30..9a03bb71c0c7877194fc89ad306cc4f073f16b48 100644
--- a/packages/desktop-client/src/components/budget/MobileBudgetTable.jsx
+++ b/packages/desktop-client/src/components/budget/MobileBudgetTable.jsx
@@ -1,5 +1,4 @@
 import React, { memo, useRef, useState } from 'react';
-import { useSelector } from 'react-redux';
 
 import memoizeOne from 'memoize-one';
 
@@ -7,6 +6,7 @@ import { rolloverBudget, reportBudget } from 'loot-core/src/client/queries';
 import * as monthUtils from 'loot-core/src/shared/months';
 
 import { useFeatureFlag } from '../../hooks/useFeatureFlag';
+import { useLocalPref } from '../../hooks/useLocalPref';
 import {
   SingleActiveEditFormProvider,
   useSingleActiveEditForm,
@@ -1133,7 +1133,6 @@ export function BudgetTable({
   onBudgetAction,
   onRefresh,
   onSwitchBudgetType,
-  savePrefs,
   pushModal,
   onEditGroup,
   onEditCategory,
@@ -1144,24 +1143,15 @@ export function BudgetTable({
   // let editMode = false; // neuter editMode -- sorry, not rewriting drag-n-drop right now
   const format = useFormat();
 
-  const mobileShowBudgetedColPref = useSelector(state => {
-    return state.prefs?.local?.toggleMobileDisplayPref || true;
-  });
-
-  const showHiddenCategories = useSelector(state => {
-    return state.prefs?.local?.['budget.showHiddenCategories'] || false;
-  });
-
-  const [showBudgetedCol, setShowBudgetedCol] = useState(
-    !mobileShowBudgetedColPref &&
-      !document.cookie.match(/mobileShowBudgetedColPref=true/),
+  const [showSpentColumn = false, setShowSpentColumnPref] = useLocalPref(
+    'mobile.showSpentColumn',
   );
 
+  const [showHiddenCategories = false, setShowHiddenCategoriesPref] =
+    useLocalPref('budget.showHiddenCategories');
+
   function toggleDisplay() {
-    setShowBudgetedCol(!showBudgetedCol);
-    if (!showBudgetedCol) {
-      savePrefs({ mobileShowBudgetedColPref: true });
-    }
+    setShowSpentColumnPref(!showSpentColumn);
   }
 
   const buttonStyle = {
@@ -1177,9 +1167,7 @@ export function BudgetTable({
   };
 
   const onToggleHiddenCategories = () => {
-    savePrefs({
-      'budget.showHiddenCategories': !showHiddenCategories,
-    });
+    setShowHiddenCategoriesPref(!showHiddenCategories);
   };
 
   return (
@@ -1245,7 +1233,7 @@ export function BudgetTable({
             />
           )}
           <View style={{ flex: 1 }} />
-          {(show3Cols || showBudgetedCol) && (
+          {(show3Cols || !showSpentColumn) && (
             <Button
               type="bare"
               disabled={show3Cols}
@@ -1255,7 +1243,7 @@ export function BudgetTable({
                 padding: '0 8px',
                 margin: '0 -8px',
                 background:
-                  showBudgetedCol && !show3Cols
+                  !showSpentColumn && !show3Cols
                     ? `linear-gradient(-45deg, ${theme.formInputBackgroundSelection} 8px, transparent 0)`
                     : null,
               }}
@@ -1292,7 +1280,7 @@ export function BudgetTable({
               </View>
             </Button>
           )}
-          {(show3Cols || !showBudgetedCol) && (
+          {(show3Cols || showSpentColumn) && (
             <Button
               type="bare"
               disabled={show3Cols}
@@ -1300,7 +1288,7 @@ export function BudgetTable({
               style={{
                 ...buttonStyle,
                 background:
-                  !showBudgetedCol && !show3Cols
+                  showSpentColumn && !show3Cols
                     ? `linear-gradient(45deg, ${theme.formInputBackgroundSelection} 8px, transparent 0)`
                     : null,
               }}
@@ -1372,7 +1360,7 @@ export function BudgetTable({
               <BudgetGroups
                 type={type}
                 categoryGroups={categoryGroups}
-                showBudgetedCol={showBudgetedCol}
+                showBudgetedCol={!showSpentColumn}
                 show3Cols={show3Cols}
                 showHiddenCategories={showHiddenCategories}
                 // gestures={gestures}
@@ -1407,7 +1395,7 @@ export function BudgetTable({
               <BudgetGroups
                 type={type}
                 categoryGroups={categoryGroups}
-                showBudgetedCol={showBudgetedCol}
+                showBudgetedCol={!showSpentColumn}
                 show3Cols={show3Cols}
                 showHiddenCategories={showHiddenCategories}
                 // gestures={gestures}
diff --git a/packages/desktop-client/src/components/budget/MonthCountSelector.tsx b/packages/desktop-client/src/components/budget/MonthCountSelector.tsx
index 737dcdc69375726db5df08252f46d4aabb0a6b80..d64ed0082b8170db415d88553ee6a19c48ddfb2d 100644
--- a/packages/desktop-client/src/components/budget/MonthCountSelector.tsx
+++ b/packages/desktop-client/src/components/budget/MonthCountSelector.tsx
@@ -22,7 +22,7 @@ function Calendar({ color, onClick }: CalendarProps) {
 
 type MonthCountSelectorProps = {
   maxMonths: number;
-  onChange: (value: number) => Promise<void>;
+  onChange: (value: number) => void;
 };
 
 export function MonthCountSelector({
diff --git a/packages/desktop-client/src/components/budget/index.tsx b/packages/desktop-client/src/components/budget/index.tsx
index ea263830d8d228fb78b2af95724a32671f8a0adb..724a969c231f55fdb0ec9b8c0959936b23dda91d 100644
--- a/packages/desktop-client/src/components/budget/index.tsx
+++ b/packages/desktop-client/src/components/budget/index.tsx
@@ -7,35 +7,31 @@ import React, {
   useEffect,
   useRef,
 } from 'react';
-import { useSelector } from 'react-redux';
-import {
-  type NavigateFunction,
-  type PathMatch,
-  useLocation,
-  useMatch,
-} from 'react-router-dom';
-
-import { type State } from 'loot-core/client/state-types';
-import { type PrefsState } from 'loot-core/client/state-types/prefs';
-import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider';
-import { send, listen } from 'loot-core/src/platform/client/fetch';
+import { useDispatch } from 'react-redux';
+
 import {
-  addCategory,
+  addNotification,
+  applyBudgetAction,
+  createCategory,
+  createGroup,
+  deleteCategory,
+  deleteGroup,
+  getCategories,
+  loadPrefs,
   moveCategory,
   moveCategoryGroup,
+  pushModal,
   updateCategory,
-  deleteCategory,
-  addGroup,
   updateGroup,
-  deleteGroup,
-} from 'loot-core/src/shared/categories';
+} from 'loot-core/src/client/actions';
+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 GlobalPrefs, type LocalPrefs } from 'loot-core/src/types/prefs';
-import { type CategoryGroupEntity } from 'loot-core/types/models';
 
-import { type BoundActions, useActions } from '../../hooks/useActions';
 import { useCategories } from '../../hooks/useCategories';
 import { useFeatureFlag } from '../../hooks/useFeatureFlag';
+import { useGlobalPref } from '../../hooks/useGlobalPref';
+import { useLocalPref } from '../../hooks/useLocalPref';
 import { useNavigate } from '../../hooks/useNavigate';
 import { styles } from '../../style';
 import { View } from '../common/View';
@@ -75,62 +71,41 @@ type RolloverComponents = {
 
 type BudgetProps = {
   accountId?: string;
-  startMonth: LocalPrefs['budget.startMonth'];
-  collapsedPrefs: LocalPrefs['budget.collapsed'];
-  summaryCollapsed: LocalPrefs['budget.summaryCollapsed'];
-  budgetType: LocalPrefs['budgetType'];
-  maxMonths: GlobalPrefs['maxMonths'];
-  categoryGroups: CategoryGroupEntity[];
   reportComponents: ReportComponents;
   rolloverComponents: RolloverComponents;
   titlebar: TitlebarContextValue;
-  match: PathMatch<string>;
-  spreadsheet: ReturnType<typeof useSpreadsheet>;
-  navigate: NavigateFunction;
-  getCategories: BoundActions['getCategories'];
-  savePrefs: BoundActions['savePrefs'];
-  createCategory: BoundActions['createCategory'];
-  updateCategory: BoundActions['updateCategory'];
-  pushModal: BoundActions['pushModal'];
-  deleteCategory: BoundActions['deleteCategory'];
-  createGroup: BoundActions['createGroup'];
-  updateGroup: BoundActions['updateGroup'];
-  deleteGroup: BoundActions['deleteGroup'];
-  applyBudgetAction: BoundActions['applyBudgetAction'];
-  moveCategory: BoundActions['moveCategory'];
-  moveCategoryGroup: BoundActions['moveCategoryGroup'];
-  loadPrefs: BoundActions['loadPrefs'];
-  addNotification: BoundActions['addNotification'];
 };
 
 function BudgetInner(props: BudgetProps) {
   const currentMonth = monthUtils.currentMonth();
   const tableRef = useRef(null);
-
-  const [initialized, setInitialized] = useState(false);
-  const [prewarmStartMonth, setPrewarmStartMonth] = useState(
-    props.startMonth || currentMonth,
+  const spreadsheet = useSpreadsheet();
+  const dispatch = useDispatch();
+  const navigate = useNavigate();
+  const [_startMonth, setBudgetStartMonthPref] =
+    useLocalPref('budget.startMonth');
+  const startMonth = _startMonth || currentMonth;
+  const [summaryCollapsed, setSummaryCollapsedPref] = useLocalPref(
+    'budget.summaryCollapsed',
   );
+  const [_budgetType] = useLocalPref('budgetType');
+  const budgetType = _budgetType || 'rollover';
+  const [_maxMonths] = useGlobalPref('maxMonths');
+  const maxMonths = _maxMonths || 1;
 
-  const [newCategoryForGroup, setNewCategoryForGroup] = useState(null);
-  const [isAddingGroup, setIsAddingGroup] = useState(false);
-  const [collapsed, setCollapsed] = useState(props.collapsedPrefs || []);
+  const [initialized, setInitialized] = useState(false);
   const [bounds, setBounds] = useState({
     start: currentMonth,
     end: currentMonth,
   });
-  const [categoryGroups, setCategoryGroups] = useState(null);
-  const [summaryCollapsed, setSummaryCollapsed] = useState(
-    props.summaryCollapsed,
-  );
+  const { grouped: categoryGroups } = useCategories();
 
-  async function loadCategories() {
-    const result = await props.getCategories();
-    setCategoryGroups(result.grouped);
+  function loadCategories() {
+    dispatch(getCategories());
   }
 
   useEffect(() => {
-    const { titlebar, budgetType } = props;
+    const { titlebar } = props;
 
     async function run() {
       loadCategories();
@@ -140,9 +115,9 @@ function BudgetInner(props: BudgetProps) {
 
       await prewarmAllMonths(
         budgetType,
-        props.spreadsheet,
+        spreadsheet,
         { start, end },
-        prewarmStartMonth,
+        startMonth,
       );
 
       setInitialized(true);
@@ -187,10 +162,6 @@ function BudgetInner(props: BudgetProps) {
     };
   }, []);
 
-  useEffect(() => {
-    props.savePrefs({ 'budget.collapsed': collapsed });
-  }, [collapsed]);
-
   useEffect(() => {
     send('get-budget-bounds').then(({ start, end }) => {
       if (bounds.start !== start || bounds.end !== end) {
@@ -200,12 +171,10 @@ function BudgetInner(props: BudgetProps) {
   }, [props.accountId]);
 
   const onMonthSelect = async (month, numDisplayed) => {
-    setPrewarmStartMonth(month);
+    setBudgetStartMonthPref(month);
 
     const warmingMonth = month;
 
-    const startMonth = props.startMonth || currentMonth;
-
     // We could be smarter about this, but this is a good start. We
     // optimize for the case where users press the left/right button
     // to move between months. This loads the month data all at once
@@ -215,51 +184,37 @@ function BudgetInner(props: BudgetProps) {
     if (month < startMonth) {
       // pre-warm prev month
       await prewarmMonth(
-        props.budgetType,
-        props.spreadsheet,
+        budgetType,
+        spreadsheet,
         monthUtils.subMonths(month, 1),
       );
     } else if (month > startMonth) {
       // pre-warm next month
       await prewarmMonth(
-        props.budgetType,
-        props.spreadsheet,
+        budgetType,
+        spreadsheet,
         monthUtils.addMonths(month, numDisplayed),
       );
     }
 
     if (warmingMonth === month) {
-      props.savePrefs({ 'budget.startMonth': month });
+      setBudgetStartMonthPref(month);
     }
   };
 
-  const onShowNewCategory = groupId => {
-    setNewCategoryForGroup(groupId);
-    setCollapsed(state => state.filter(c => c !== groupId));
-  };
-
-  const onHideNewCategory = () => {
-    setNewCategoryForGroup(null);
-  };
-
-  const onShowNewGroup = () => {
-    setIsAddingGroup(true);
-  };
-
-  const onHideNewGroup = () => {
-    setIsAddingGroup(false);
-  };
-
   const categoryNameAlreadyExistsNotification = name => {
-    props.addNotification({
-      type: 'error',
-      message: `Category ‘${name}’ already exists in group (May be Hidden)`,
-    });
+    dispatch(
+      addNotification({
+        type: 'error',
+        message: `Category ‘${name}’ already exists in group (May be Hidden)`,
+      }),
+    );
   };
 
   const onSaveCategory = async category => {
+    const cats = await send('get-categories');
     const exists =
-      (await props.getCategories()).grouped
+      cats.grouped
         .filter(g => g.id === category.cat_group)[0]
         .categories.filter(
           c => c.name.toUpperCase() === category.name.toUpperCase(),
@@ -273,24 +228,16 @@ function BudgetInner(props: BudgetProps) {
     }
 
     if (category.id === 'new') {
-      const id = await props.createCategory(
-        category.name,
-        category.cat_group,
-        category.is_income,
-        category.hidden,
-      );
-
-      setNewCategoryForGroup(null);
-      setCategoryGroups(state =>
-        addCategory(state, {
-          ...category,
-          is_income: category.is_income ? 1 : 0,
-          id,
-        }),
+      dispatch(
+        createCategory(
+          category.name,
+          category.cat_group,
+          category.is_income,
+          category.hidden,
+        ),
       );
     } else {
-      props.updateCategory(category);
-      setCategoryGroups(state => updateCategory(state, category));
+      dispatch(updateCategory(category));
     }
   };
 
@@ -298,55 +245,26 @@ function BudgetInner(props: BudgetProps) {
     const mustTransfer = await send('must-category-transfer', { id });
 
     if (mustTransfer) {
-      props.pushModal('confirm-category-delete', {
-        category: id,
-        onDelete: transferCategory => {
-          if (id !== transferCategory) {
-            props.deleteCategory(id, transferCategory);
-
-            setCategoryGroups(state => deleteCategory(state, id));
-          }
-        },
-      });
+      dispatch(
+        pushModal('confirm-category-delete', {
+          category: id,
+          onDelete: transferCategory => {
+            if (id !== transferCategory) {
+              dispatch(deleteCategory(id, transferCategory));
+            }
+          },
+        }),
+      );
     } else {
-      props.deleteCategory(id);
-
-      setCategoryGroups(state => deleteCategory(state, id));
+      dispatch(deleteCategory(id));
     }
   };
 
-  const groupNameAlreadyExistsNotification = group => {
-    props.addNotification({
-      type: 'error',
-      message: `A ${group.hidden ? 'hidden ' : ''}’${group.name}’ category group already exists.`,
-    });
-  };
-
-  const onSaveGroup = async group => {
-    const categories = await props.getCategories();
-    const matchingGroups = categories.grouped
-      .filter(g => g.name.toUpperCase() === group.name.toUpperCase())
-      .filter(g => group.id === 'new' || group.id !== g.id);
-
-    if (matchingGroups.length > 0) {
-      groupNameAlreadyExistsNotification(matchingGroups[0]);
-      return;
-    }
-
+  const onSaveGroup = group => {
     if (group.id === 'new') {
-      const id = await props.createGroup(group.name);
-      setIsAddingGroup(false);
-      setCategoryGroups(state =>
-        addGroup(state, {
-          ...group,
-          is_income: 0,
-          categories: group.categories || [],
-          id,
-        }),
-      );
+      dispatch(createGroup(group.name));
     } else {
-      props.updateGroup(group);
-      setCategoryGroups(state => updateGroup(state, group));
+      dispatch(updateGroup(group));
     }
   };
 
@@ -362,27 +280,25 @@ function BudgetInner(props: BudgetProps) {
     }
 
     if (mustTransfer) {
-      props.pushModal('confirm-category-delete', {
-        group: id,
-        onDelete: transferCategory => {
-          props.deleteGroup(id, transferCategory);
-
-          setCategoryGroups(state => deleteGroup(state, id));
-        },
-      });
+      dispatch(
+        pushModal('confirm-category-delete', {
+          group: id,
+          onDelete: transferCategory => {
+            dispatch(deleteGroup(id, transferCategory));
+          },
+        }),
+      );
     } else {
-      props.deleteGroup(id);
-
-      setCategoryGroups(state => deleteGroup(state, id));
+      dispatch(deleteGroup(id));
     }
   };
 
   const onBudgetAction = (month, type, args) => {
-    props.applyBudgetAction(month, type, args);
+    dispatch(applyBudgetAction(month, type, args));
   };
 
   const onShowActivity = (categoryName, categoryId, month) => {
-    props.navigate('/accounts', {
+    navigate('/accounts', {
       state: {
         goBack: true,
         filterName: `${categoryName} (${monthUtils.format(
@@ -398,7 +314,7 @@ function BudgetInner(props: BudgetProps) {
   };
 
   const onReorderCategory = async sortInfo => {
-    const cats = await props.getCategories();
+    const cats = await send('get-categories');
     const moveCandidate = cats.list.filter(c => c.id === sortInfo.id)[0];
     const exists =
       cats.grouped
@@ -413,23 +329,15 @@ function BudgetInner(props: BudgetProps) {
       return;
     }
 
-    props.moveCategory(sortInfo.id, sortInfo.groupId, sortInfo.targetId);
-    setCategoryGroups(state =>
-      moveCategory(state, sortInfo.id, sortInfo.groupId, sortInfo.targetId),
-    );
+    dispatch(moveCategory(sortInfo.id, sortInfo.groupId, sortInfo.targetId));
   };
 
   const onReorderGroup = async sortInfo => {
-    props.moveCategoryGroup(sortInfo.id, sortInfo.targetId);
-    setCategoryGroups(state =>
-      moveCategoryGroup(state, sortInfo.id, sortInfo.targetId),
-    );
+    dispatch(moveCategoryGroup(sortInfo.id, sortInfo.targetId));
   };
 
   const onToggleCollapse = () => {
-    const collapsed = !summaryCollapsed;
-    setSummaryCollapsed(collapsed);
-    props.savePrefs({ 'budget.summaryCollapsed': collapsed });
+    setSummaryCollapsedPref(!summaryCollapsed);
   };
 
   const onTitlebarEvent = async ({ type, payload }: TitlebarMessage) => {
@@ -437,10 +345,12 @@ function BudgetInner(props: BudgetProps) {
       case SWITCH_BUDGET_MESSAGE_TYPE: {
         await switchBudgetType(
           payload.newBudgetType,
-          props.spreadsheet,
+          spreadsheet,
           bounds,
-          prewarmStartMonth,
-          () => props.loadPrefs(),
+          startMonth,
+          async () => {
+            dispatch(loadPrefs());
+          },
         );
         break;
       }
@@ -448,23 +358,14 @@ function BudgetInner(props: BudgetProps) {
     }
   };
 
-  const {
-    maxMonths: originalMaxMonths,
-    budgetType: type,
-    reportComponents,
-    rolloverComponents,
-  } = props;
-
-  const maxMonths = originalMaxMonths || 1;
+  const { reportComponents, rolloverComponents } = props;
 
   if (!initialized || !categoryGroups) {
     return null;
   }
 
-  const startMonth = props.startMonth || currentMonth;
-
   let table;
-  if (type === 'report') {
+  if (budgetType === 'report') {
     table = (
       <ReportProvider
         summaryCollapsed={summaryCollapsed}
@@ -473,22 +374,13 @@ function BudgetInner(props: BudgetProps) {
       >
         <DynamicBudgetTable
           ref={tableRef}
-          type={type}
-          categoryGroups={categoryGroups}
-          prewarmStartMonth={prewarmStartMonth}
+          type={budgetType}
+          prewarmStartMonth={startMonth}
           startMonth={startMonth}
           monthBounds={bounds}
           maxMonths={maxMonths}
-          collapsed={collapsed}
-          setCollapsed={setCollapsed}
-          newCategoryForGroup={newCategoryForGroup}
-          isAddingGroup={isAddingGroup}
           dataComponents={reportComponents}
           onMonthSelect={onMonthSelect}
-          onShowNewCategory={onShowNewCategory}
-          onHideNewCategory={onHideNewCategory}
-          onShowNewGroup={onShowNewGroup}
-          onHideNewGroup={onHideNewGroup}
           onDeleteCategory={onDeleteCategory}
           onDeleteGroup={onDeleteGroup}
           onSaveCategory={onSaveCategory}
@@ -503,29 +395,19 @@ function BudgetInner(props: BudgetProps) {
   } else {
     table = (
       <RolloverContext
-        categoryGroups={categoryGroups}
         summaryCollapsed={summaryCollapsed}
         onBudgetAction={onBudgetAction}
         onToggleSummaryCollapse={onToggleCollapse}
       >
         <DynamicBudgetTable
           ref={tableRef}
-          type={type}
-          categoryGroups={categoryGroups}
-          prewarmStartMonth={prewarmStartMonth}
+          type={budgetType}
+          prewarmStartMonth={startMonth}
           startMonth={startMonth}
           monthBounds={bounds}
           maxMonths={maxMonths}
-          collapsed={collapsed}
-          setCollapsed={setCollapsed}
-          newCategoryForGroup={newCategoryForGroup}
-          isAddingGroup={isAddingGroup}
           dataComponents={rolloverComponents}
           onMonthSelect={onMonthSelect}
-          onShowNewCategory={onShowNewCategory}
-          onHideNewCategory={onHideNewCategory}
-          onShowNewGroup={onShowNewGroup}
-          onHideNewGroup={onHideNewGroup}
           onDeleteCategory={onDeleteCategory}
           onDeleteGroup={onDeleteGroup}
           onSaveCategory={onSaveCategory}
@@ -553,32 +435,7 @@ const RolloverBudgetSummary = memo<{ month: string }>(props => {
 });
 
 export function Budget() {
-  const startMonth = useSelector<
-    State,
-    PrefsState['local']['budget.startMonth']
-  >(state => state.prefs.local['budget.startMonth']);
-  const collapsedPrefs = useSelector<
-    State,
-    PrefsState['local']['budget.collapsed']
-  >(state => state.prefs.local['budget.collapsed']);
-  const summaryCollapsed = useSelector<
-    State,
-    PrefsState['local']['budget.summaryCollapsed']
-  >(state => state.prefs.local['budget.summaryCollapsed']);
-  const budgetType = useSelector<State, PrefsState['local']['budgetType']>(
-    state => state.prefs.local.budgetType || 'rollover',
-  );
-  const maxMonths = useSelector<State, PrefsState['global']['maxMonths']>(
-    state => state.prefs.global.maxMonths,
-  );
-  const { grouped: categoryGroups } = useCategories();
-
-  const actions = useActions();
-  const spreadsheet = useSpreadsheet();
   const titlebar = useContext(TitlebarContext);
-  const location = useLocation();
-  const match = useMatch(location.pathname);
-  const navigate = useNavigate();
 
   const reportComponents = useMemo<ReportComponents>(
     () => ({
@@ -620,19 +477,9 @@ export function Budget() {
       }}
     >
       <BudgetInner
-        startMonth={startMonth}
-        collapsedPrefs={collapsedPrefs}
-        summaryCollapsed={summaryCollapsed}
-        budgetType={budgetType}
-        maxMonths={maxMonths}
-        categoryGroups={categoryGroups}
-        {...actions}
         reportComponents={reportComponents}
         rolloverComponents={rolloverComponents}
-        spreadsheet={spreadsheet}
         titlebar={titlebar}
-        navigate={navigate}
-        match={match}
       />
     </View>
   );
diff --git a/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx b/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx
index 636953b16ac16f4de04fed3d1976caf6f4942e68..fcbc714cd557acc9c8bffe95cbdc43e9f2ad9a7e 100644
--- a/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx
+++ b/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx
@@ -6,14 +6,12 @@ import * as monthUtils from 'loot-core/src/shared/months';
 const Context = createContext(null);
 
 type RolloverContextProps = {
-  categoryGroups: unknown[];
   summaryCollapsed: boolean;
   onBudgetAction: (idx: number, action: string, arg?: unknown) => void;
   onToggleSummaryCollapse: () => void;
   children: ReactNode;
 };
 export function RolloverContext({
-  categoryGroups,
   summaryCollapsed,
   onBudgetAction,
   onToggleSummaryCollapse,
@@ -25,7 +23,6 @@ export function RolloverContext({
     <Context.Provider
       value={{
         currentMonth,
-        categoryGroups,
         summaryCollapsed,
         onBudgetAction,
         onToggleSummaryCollapse,
diff --git a/packages/desktop-client/src/components/filters/AppliedFilters.tsx b/packages/desktop-client/src/components/filters/AppliedFilters.tsx
index 1f5ead431ea702da4d00c318ed82bf6e01fb99bd..eac0510a1ab7713ac0fb43f7598181819abe3aec 100644
--- a/packages/desktop-client/src/components/filters/AppliedFilters.tsx
+++ b/packages/desktop-client/src/components/filters/AppliedFilters.tsx
@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { type RuleConditionEntity } from 'loot-core/types/models';
+import { type RuleConditionEntity } from 'loot-core/src/types/models';
 
 import { View } from '../common/View';
 
diff --git a/packages/desktop-client/src/components/filters/FiltersMenu.jsx b/packages/desktop-client/src/components/filters/FiltersMenu.jsx
index f4650c5a44fa68e3a6be3edce75496eda84eb551..18889c5159c3a7117b8e5c161f9f6270aa858829 100644
--- a/packages/desktop-client/src/components/filters/FiltersMenu.jsx
+++ b/packages/desktop-client/src/components/filters/FiltersMenu.jsx
@@ -1,5 +1,4 @@
 import React, { useState, useRef, useEffect, useReducer } from 'react';
-import { useSelector } from 'react-redux';
 
 import { FocusScope } from '@react-aria/focus';
 import {
@@ -21,6 +20,7 @@ import {
 } from 'loot-core/src/shared/rules';
 import { titleFirst } from 'loot-core/src/shared/util';
 
+import { useDateFormat } from '../../hooks/useDateFormat';
 import { theme } from '../../style';
 import { Button } from '../common/Button';
 import { HoverTarget } from '../common/HoverTarget';
@@ -246,11 +246,7 @@ function ConfigureField({
 export function FilterButton({ onApply, compact, hover }) {
   const filters = useFilters();
 
-  const { dateFormat } = useSelector(state => {
-    return {
-      dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy',
-    };
-  });
+  const dateFormat = useDateFormat() || 'MM/dd/yyyy';
 
   const [state, dispatch] = useReducer(
     (state, action) => {
diff --git a/packages/desktop-client/src/components/modals/CloseAccount.tsx b/packages/desktop-client/src/components/modals/CloseAccount.tsx
index e5bc05b8df6d12b2df5be6a5efb15100c4e46a79..9266c584bc28c38905674667dda818c9618ca61d 100644
--- a/packages/desktop-client/src/components/modals/CloseAccount.tsx
+++ b/packages/desktop-client/src/components/modals/CloseAccount.tsx
@@ -2,12 +2,11 @@
 import React, { useState } from 'react';
 
 import { integerToCurrency } from 'loot-core/src/shared/util';
-import {
-  type AccountEntity,
-  type CategoryGroupEntity,
-} from 'loot-core/src/types/models';
+import { type AccountEntity } from 'loot-core/src/types/models';
 
+import { useAccounts } from '../../hooks/useAccounts';
 import { type BoundActions } from '../../hooks/useActions';
+import { useCategories } from '../../hooks/useCategories';
 import { theme } from '../../style';
 import { AccountAutocomplete } from '../autocomplete/AccountAutocomplete';
 import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete';
@@ -35,8 +34,6 @@ function needsCategory(
 
 type CloseAccountProps = {
   account: AccountEntity;
-  accounts: AccountEntity[];
-  categoryGroups: CategoryGroupEntity[];
   balance: number;
   canDelete: boolean;
   actions: BoundActions;
@@ -45,8 +42,6 @@ type CloseAccountProps = {
 
 export function CloseAccount({
   account,
-  accounts,
-  categoryGroups,
   balance,
   canDelete,
   actions,
@@ -58,6 +53,8 @@ export function CloseAccount({
 
   const [transferError, setTransferError] = useState(false);
   const [categoryError, setCategoryError] = useState(false);
+  const accounts = useAccounts().filter(a => a.closed === 0);
+  const { grouped: categoryGroups } = useCategories();
 
   return (
     <Modal
diff --git a/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx b/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx
index 803e950035099523fb201db698b8e0a50be249a4..a448f351ca077f935ae3f9efbe451ee5992c1361 100644
--- a/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx
+++ b/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx
@@ -1,8 +1,7 @@
 // @ts-strict-ignore
 import React, { useState } from 'react';
 
-import { type CategoryGroupEntity } from 'loot-core/src/types/models';
-
+import { useCategories } from '../../hooks/useCategories';
 import { theme } from '../../style';
 import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete';
 import { Block } from '../common/Block';
@@ -14,21 +13,22 @@ import { type CommonModalProps } from '../Modals';
 
 type ConfirmCategoryDeleteProps = {
   modalProps: CommonModalProps;
-  category: CategoryGroupEntity;
-  group: CategoryGroupEntity;
-  categoryGroups: CategoryGroupEntity[];
+  category: string;
+  group: string;
   onDelete: (categoryId: string) => void;
 };
 
 export function ConfirmCategoryDelete({
   modalProps,
-  category,
-  group,
-  categoryGroups,
+  group: groupId,
+  category: categoryId,
   onDelete,
 }: ConfirmCategoryDeleteProps) {
   const [transferCategory, setTransferCategory] = useState<string | null>(null);
   const [error, setError] = useState<string | null>(null);
+  const { grouped: categoryGroups, list: categories } = useCategories();
+  const group = categoryGroups.find(g => g.id === groupId);
+  const category = categories.find(c => c.id === categoryId);
 
   const renderError = (error: string) => {
     let msg: string;
diff --git a/packages/desktop-client/src/components/modals/EditField.jsx b/packages/desktop-client/src/components/modals/EditField.jsx
index fa2b492c4c84e8dc225430db107865a146fd8baa..0e2d3f1ff331a792a7c3c171b804e3c316a739a7 100644
--- a/packages/desktop-client/src/components/modals/EditField.jsx
+++ b/packages/desktop-client/src/components/modals/EditField.jsx
@@ -1,13 +1,15 @@
 import React from 'react';
-import { useSelector } from 'react-redux';
 
 import { parseISO, format as formatDate, parse as parseDate } from 'date-fns';
 
 import { currentDay, dayFromDate } from 'loot-core/src/shared/months';
 import { amountToInteger } from 'loot-core/src/shared/util';
 
+import { useAccounts } from '../../hooks/useAccounts';
 import { useActions } from '../../hooks/useActions';
 import { useCategories } from '../../hooks/useCategories';
+import { useDateFormat } from '../../hooks/useDateFormat';
+import { usePayees } from '../../hooks/usePayees';
 import { SvgAdd } from '../../icons/v1';
 import { useResponsive } from '../../ResponsiveProvider';
 import { styles, theme } from '../../style';
@@ -38,12 +40,10 @@ function CreatePayeeIcon(props) {
 }
 
 export function EditField({ modalProps, name, onSubmit, onClose }) {
-  const dateFormat = useSelector(
-    state => state.prefs.local.dateFormat || 'MM/dd/yyyy',
-  );
+  const dateFormat = useDateFormat() || 'MM/dd/yyyy';
   const { grouped: categoryGroups } = useCategories();
-  const accounts = useSelector(state => state.queries.accounts);
-  const payees = useSelector(state => state.queries.payees);
+  const accounts = useAccounts();
+  const payees = usePayees();
 
   const { createPayee } = useActions();
 
diff --git a/packages/desktop-client/src/components/modals/EditRule.jsx b/packages/desktop-client/src/components/modals/EditRule.jsx
index e7ebb2094700f80cd5ab9b8ab5c587a2f0f65233..83cd8799d0e79d369e47b6ac506db61ce7c835ff 100644
--- a/packages/desktop-client/src/components/modals/EditRule.jsx
+++ b/packages/desktop-client/src/components/modals/EditRule.jsx
@@ -1,5 +1,5 @@
 import React, { useState, useEffect, useRef, useCallback } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { useDispatch } from 'react-redux';
 
 import { v4 as uuid } from 'uuid';
 
@@ -28,6 +28,7 @@ import {
   amountToInteger,
 } from 'loot-core/src/shared/util';
 
+import { useDateFormat } from '../../hooks/useDateFormat';
 import { useFeatureFlag } from '../../hooks/useFeatureFlag';
 import { useSelected, SelectedProvider } from '../../hooks/useSelected';
 import { SvgDelete, SvgAdd, SvgSubtract } from '../../icons/v0';
@@ -268,9 +269,7 @@ function formatAmount(amount) {
 }
 
 function ScheduleDescription({ id }) {
-  const dateFormat = useSelector(state => {
-    return state.prefs.local.dateFormat || 'MM/dd/yyyy';
-  });
+  const dateFormat = useDateFormat() || 'MM/dd/yyyy';
   const scheduleData = useSchedules({
     transform: useCallback(q => q.filter({ id }), []),
   });
diff --git a/packages/desktop-client/src/components/modals/ImportTransactions.jsx b/packages/desktop-client/src/components/modals/ImportTransactions.jsx
index 89a564b7014b845c0c251e963b8bd92a1b68b608..3bd05e3008cfe13e24474c3b4ced92c957d3bcd1 100644
--- a/packages/desktop-client/src/components/modals/ImportTransactions.jsx
+++ b/packages/desktop-client/src/components/modals/ImportTransactions.jsx
@@ -1,5 +1,4 @@
 import React, { useState, useEffect, useMemo } from 'react';
-import { useSelector } from 'react-redux';
 
 import * as d from 'date-fns';
 
@@ -11,6 +10,8 @@ import {
 } from 'loot-core/src/shared/util';
 
 import { useActions } from '../../hooks/useActions';
+import { useDateFormat } from '../../hooks/useDateFormat';
+import { useLocalPrefs } from '../../hooks/useLocalPrefs';
 import { theme, styles } from '../../style';
 import { Button, ButtonWithLoading } from '../common/Button';
 import { Input } from '../common/Input';
@@ -703,10 +704,8 @@ function FieldMappings({
 }
 
 export function ImportTransactions({ modalProps, options }) {
-  const dateFormat = useSelector(
-    state => state.prefs.local.dateFormat || 'MM/dd/yyyy',
-  );
-  const prefs = useSelector(state => state.prefs.local);
+  const dateFormat = useDateFormat() || 'MM/dd/yyyy';
+  const prefs = useLocalPrefs();
   const { parseTransactions, importTransactions, getPayees, savePrefs } =
     useActions();
 
diff --git a/packages/desktop-client/src/components/modals/LoadBackup.jsx b/packages/desktop-client/src/components/modals/LoadBackup.jsx
index d430346b22315568005aee49fe59e0729f27c7ff..e09f783322a8a35283491e310bc6469f518d5a58 100644
--- a/packages/desktop-client/src/components/modals/LoadBackup.jsx
+++ b/packages/desktop-client/src/components/modals/LoadBackup.jsx
@@ -2,6 +2,7 @@ import React, { Component, useState, useEffect } from 'react';
 
 import { send, listen, unlisten } from 'loot-core/src/platform/client/fetch';
 
+import { useLocalPref } from '../../hooks/useLocalPref';
 import { theme } from '../../style';
 import { Block } from '../common/Block';
 import { Button } from '../common/Button';
@@ -55,10 +56,12 @@ export function LoadBackup({
   modalProps,
 }) {
   const [backups, setBackups] = useState([]);
+  const [prefsBudgetId] = useLocalPref('id');
+  const budgetIdToLoad = budgetId || prefsBudgetId;
 
   useEffect(() => {
-    send('backups-get', { id: budgetId }).then(setBackups);
-  }, [budgetId]);
+    send('backups-get', { id: budgetIdToLoad }).then(setBackups);
+  }, [budgetIdToLoad]);
 
   useEffect(() => {
     if (watchUpdates) {
@@ -93,7 +96,9 @@ export function LoadBackup({
                 </Block>
                 <Button
                   type="primary"
-                  onClick={() => actions.loadBackup(budgetId, latestBackup.id)}
+                  onClick={() =>
+                    actions.loadBackup(budgetIdToLoad, latestBackup.id)
+                  }
                 >
                   Revert to original version
                 </Button>
@@ -125,7 +130,7 @@ export function LoadBackup({
           ) : (
             <BackupTable
               backups={previousBackups}
-              onSelect={id => actions.loadBackup(budgetId, id)}
+              onSelect={id => actions.loadBackup(budgetIdToLoad, id)}
             />
           )}
         </View>
diff --git a/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx b/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx
index 5e36e4f9d994ea8c9237132229fdff820de338b6..33ca3d70328f19ce281c4a2474dde4d9e341f39b 100644
--- a/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx
+++ b/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx
@@ -4,6 +4,7 @@ import { useDispatch, useSelector } from 'react-redux';
 import { replaceModal } from 'loot-core/src/client/actions/modals';
 import { send } from 'loot-core/src/platform/client/fetch';
 
+import { usePayees } from '../../hooks/usePayees';
 import { theme } from '../../style';
 import { Information } from '../alerts';
 import { Button } from '../common/Button';
@@ -15,10 +16,8 @@ import { View } from '../common/View';
 const highlightStyle = { color: theme.pageTextPositive };
 
 export function MergeUnusedPayees({ modalProps, payeeIds, targetPayeeId }) {
-  const { payees: allPayees, modalStack } = useSelector(state => ({
-    payees: state.queries.payees,
-    modalStack: state.modals.modalStack,
-  }));
+  const allPayees = usePayees();
+  const modalStack = useSelector(state => state.modals.modalStack);
   const isEditingRule = !!modalStack.find(m => m.name === 'edit-rule');
   const dispatch = useDispatch();
   const [shouldCreateRule, setShouldCreateRule] = useState(true);
diff --git a/packages/desktop-client/src/components/modals/SelectLinkedAccounts.jsx b/packages/desktop-client/src/components/modals/SelectLinkedAccounts.jsx
index 8d8d82475bc1e5f7b4b448eb0a3b1a3e3350647c..8f74791de538268e86f47962a5c19ab0830fe544 100644
--- a/packages/desktop-client/src/components/modals/SelectLinkedAccounts.jsx
+++ b/packages/desktop-client/src/components/modals/SelectLinkedAccounts.jsx
@@ -1,5 +1,6 @@
 import React, { useState } from 'react';
 
+import { useAccounts } from '../../hooks/useAccounts';
 import { theme } from '../../style';
 import { Autocomplete } from '../autocomplete/Autocomplete';
 import { Button } from '../common/Button';
@@ -14,10 +15,10 @@ export function SelectLinkedAccounts({
   modalProps,
   requisitionId,
   externalAccounts,
-  localAccounts,
   actions,
   syncSource,
 }) {
+  const localAccounts = useAccounts().filter(a => a.closed === 0);
   const [chosenAccounts, setChosenAccounts] = useState(() => {
     return Object.fromEntries(
       localAccounts
diff --git a/packages/desktop-client/src/components/modals/SwitchBudgetType.tsx b/packages/desktop-client/src/components/modals/SwitchBudgetType.tsx
index 80aada7a8d5376c2aff27f9446f010f62d86346f..eb04d7e86feba349ecb2c4eec6c3decd6fceb1d0 100644
--- a/packages/desktop-client/src/components/modals/SwitchBudgetType.tsx
+++ b/packages/desktop-client/src/components/modals/SwitchBudgetType.tsx
@@ -1,10 +1,7 @@
 // @ts-strict-ignore
 import React from 'react';
-import { useSelector } from 'react-redux';
-
-import { type State } from 'loot-core/src/client/state-types';
-import { type PrefsState } from 'loot-core/src/client/state-types/prefs';
 
+import { useLocalPref } from '../../hooks/useLocalPref';
 import { Button } from '../common/Button';
 import { ExternalLink } from '../common/ExternalLink';
 import { Modal } from '../common/Modal';
@@ -21,9 +18,7 @@ export function SwitchBudgetType({
   modalProps,
   onSwitch,
 }: SwitchBudgetTypeProps) {
-  const budgetType = useSelector<State, PrefsState['local']['budgetType']>(
-    state => state.prefs.local.budgetType,
-  );
+  const [budgetType] = useLocalPref('budgetType');
   return (
     <Modal title="Switch budget type?" {...modalProps}>
       {() => (
diff --git a/packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx b/packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx
index a828c5e73f2774647cf4cc3253b63a875409ee8f..55ea8bc78ae0e8955018bb4468d755360108ef0a 100644
--- a/packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx
+++ b/packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx
@@ -6,11 +6,12 @@ import { applyChanges } from 'loot-core/src/shared/util';
 
 import { useActions } from '../../hooks/useActions';
 import { useCategories } from '../../hooks/useCategories';
+import { usePayees } from '../../hooks/usePayees';
 
 import { ManagePayees } from './ManagePayees';
 
 export function ManagePayeesWithData({ initialSelectedIds }) {
-  const initialPayees = useSelector(state => state.queries.payees);
+  const initialPayees = usePayees();
   const lastUndoState = useSelector(state => state.app.lastUndoState);
   const { grouped: categoryGroups } = useCategories();
 
diff --git a/packages/desktop-client/src/components/reports/Overview.jsx b/packages/desktop-client/src/components/reports/Overview.jsx
index e0b8163d6dadabe7278708b412e5c66b3ea51a86..539aeda890fa57448a3ef9ed7516e4d0f092cd48 100644
--- a/packages/desktop-client/src/components/reports/Overview.jsx
+++ b/packages/desktop-client/src/components/reports/Overview.jsx
@@ -1,8 +1,8 @@
 import React from 'react';
-import { useSelector } from 'react-redux';
 
 import { useReports } from 'loot-core/src/client/data-hooks/reports';
 
+import { useAccounts } from '../../hooks/useAccounts';
 import { useFeatureFlag } from '../../hooks/useFeatureFlag';
 import { styles } from '../../style';
 import { View } from '../common/View';
@@ -18,7 +18,7 @@ export function Overview() {
 
   const customReportsFeatureFlag = useFeatureFlag('customReports');
 
-  const accounts = useSelector(state => state.queries.accounts);
+  const accounts = useAccounts();
   return (
     <View
       style={{
diff --git a/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx b/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx
index a261be8e3537ba7c2ed2a493377872cc1617f7b0..54b1420301fe7a17b177765ac5b52b563f6c5f6e 100644
--- a/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx
@@ -13,13 +13,13 @@ import {
   ResponsiveContainer,
 } from 'recharts';
 
-import { usePrivacyMode } from 'loot-core/src/client/privacy';
 import {
   amountToCurrency,
   amountToCurrencyNoDecimal,
 } from 'loot-core/src/shared/util';
 import { type GroupedEntity } from 'loot-core/src/types/models/reports';
 
+import { usePrivacyMode } from '../../../hooks/usePrivacyMode';
 import { theme } from '../../../style';
 import { type CSSProperties } from '../../../style';
 import { AlignedText } from '../../common/AlignedText';
diff --git a/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx b/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx
index 3d81b5ced335564289f0ec489fe141699e634bd8..6263c66513c0a9c5b7c1c03e1d221fdbf06ab240 100644
--- a/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx
@@ -15,13 +15,13 @@ import {
   ResponsiveContainer,
 } from 'recharts';
 
-import { usePrivacyMode } from 'loot-core/src/client/privacy';
 import {
   amountToCurrency,
   amountToCurrencyNoDecimal,
 } from 'loot-core/src/shared/util';
 import { type GroupedEntity } from 'loot-core/src/types/models/reports';
 
+import { usePrivacyMode } from '../../../hooks/usePrivacyMode';
 import { theme } from '../../../style';
 import { type CSSProperties } from '../../../style';
 import { AlignedText } from '../../common/AlignedText';
diff --git a/packages/desktop-client/src/components/reports/graphs/CashFlowGraph.tsx b/packages/desktop-client/src/components/reports/graphs/CashFlowGraph.tsx
index e186a70b64b93ad89184a33c2c49fe07e160819f..8334fa9de80b1b4c452bb75cd5f23ff7340a1864 100644
--- a/packages/desktop-client/src/components/reports/graphs/CashFlowGraph.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/CashFlowGraph.tsx
@@ -15,12 +15,12 @@ import {
   type TooltipProps,
 } from 'recharts';
 
-import { usePrivacyMode } from 'loot-core/src/client/privacy';
 import {
   amountToCurrency,
   amountToCurrencyNoDecimal,
 } from 'loot-core/src/shared/util';
 
+import { usePrivacyMode } from '../../../hooks/usePrivacyMode';
 import { theme } from '../../../style';
 import { AlignedText } from '../../common/AlignedText';
 import { chartTheme } from '../chart-theme';
diff --git a/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx b/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx
index ab2fcf993b3ad601e69194a0e1e089a8f1de3556..777f4be4c7cfb762ee1c553e7a92f502b194f0c0 100644
--- a/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx
@@ -13,13 +13,13 @@ import {
   ResponsiveContainer,
 } from 'recharts';
 
-import { usePrivacyMode } from 'loot-core/src/client/privacy';
 import {
   amountToCurrency,
   amountToCurrencyNoDecimal,
 } from 'loot-core/src/shared/util';
 import { type GroupedEntity } from 'loot-core/src/types/models/reports';
 
+import { usePrivacyMode } from '../../../hooks/usePrivacyMode';
 import { theme } from '../../../style';
 import { type CSSProperties } from '../../../style';
 import { AlignedText } from '../../common/AlignedText';
diff --git a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx
index f786572ef3df8f56470d774c133fb5da6cf7cc6c..139f4716f95070e22427b4a0378337327bbd62de 100644
--- a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx
+++ b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx
@@ -1,18 +1,17 @@
 import React, { useState, useEffect, useMemo } from 'react';
-import { useSelector } from 'react-redux';
 import { useLocation } from 'react-router-dom';
 
 import * as d from 'date-fns';
 
-import { useCachedAccounts } from 'loot-core/src/client/data-hooks/accounts';
-import { useCachedPayees } from 'loot-core/src/client/data-hooks/payees';
 import { send } from 'loot-core/src/platform/client/fetch';
 import * as monthUtils from 'loot-core/src/shared/months';
 import { amountToCurrency } from 'loot-core/src/shared/util';
 
-import { useActions } from '../../../hooks/useActions';
+import { useAccounts } from '../../../hooks/useAccounts';
 import { useCategories } from '../../../hooks/useCategories';
 import { useFilters } from '../../../hooks/useFilters';
+import { useLocalPref } from '../../../hooks/useLocalPref';
+import { usePayees } from '../../../hooks/usePayees';
 import { theme, styles } from '../../../style';
 import { AlignedText } from '../../common/AlignedText';
 import { Block } from '../../common/Block';
@@ -36,13 +35,12 @@ import { fromDateRepr } from '../util';
 export function CustomReport() {
   const categories = useCategories();
 
-  const viewLegend =
-    useSelector(state => state.prefs.local?.reportsViewLegend) || false;
-  const viewSummary =
-    useSelector(state => state.prefs.local?.reportsViewSummary) || false;
-  const viewLabels =
-    useSelector(state => state.prefs.local?.reportsViewLabel) || false;
-  const { savePrefs } = useActions();
+  const [viewLegend = false, setViewLegendPref] =
+    useLocalPref('reportsViewLegend');
+  const [viewSummary = false, setViewSummaryPref] =
+    useLocalPref('reportsViewSummary');
+  const [viewLabels = false, setViewLabelsPref] =
+    useLocalPref('reportsViewLabel');
 
   const {
     filters,
@@ -126,8 +124,8 @@ export function CustomReport() {
   }, []);
 
   const balanceTypeOp = ReportOptions.balanceTypeMap.get(balanceType);
-  const payees = useCachedPayees();
-  const accounts = useCachedAccounts();
+  const payees = usePayees();
+  const accounts = useAccounts();
 
   const getGroupData = useMemo(() => {
     return createGroupedSpreadsheet({
@@ -235,13 +233,13 @@ export function CustomReport() {
 
   const onChangeViews = (viewType, status) => {
     if (viewType === 'viewLegend') {
-      savePrefs({ reportsViewLegend: status ?? !viewLegend });
+      setViewLegendPref(status ?? !viewLegend);
     }
     if (viewType === 'viewSummary') {
-      savePrefs({ reportsViewSummary: !viewSummary });
+      setViewSummaryPref(!viewSummary);
     }
     if (viewType === 'viewLabels') {
-      savePrefs({ reportsViewLabel: !viewLabels });
+      setViewLabelsPref(!viewLabels);
     }
   };
 
diff --git a/packages/desktop-client/src/components/reports/reports/NetWorth.jsx b/packages/desktop-client/src/components/reports/reports/NetWorth.jsx
index 5bdd779b24f213017d33ced0f20a1b4f25c41af2..4dbec9a8e056aa319976eb2b033b070c574b11eb 100644
--- a/packages/desktop-client/src/components/reports/reports/NetWorth.jsx
+++ b/packages/desktop-client/src/components/reports/reports/NetWorth.jsx
@@ -1,5 +1,4 @@
 import React, { useState, useEffect, useMemo } from 'react';
-import { useSelector } from 'react-redux';
 
 import * as d from 'date-fns';
 
@@ -7,6 +6,7 @@ import { send } from 'loot-core/src/platform/client/fetch';
 import * as monthUtils from 'loot-core/src/shared/months';
 import { integerToCurrency } from 'loot-core/src/shared/util';
 
+import { useAccounts } from '../../../hooks/useAccounts';
 import { useFilters } from '../../../hooks/useFilters';
 import { theme, styles } from '../../../style';
 import { Paragraph } from '../../common/Paragraph';
@@ -20,7 +20,7 @@ import { useReport } from '../useReport';
 import { fromDateRepr } from '../util';
 
 export function NetWorth() {
-  const accounts = useSelector(state => state.queries.accounts);
+  const accounts = useAccounts();
   const {
     filters,
     saved,
diff --git a/packages/desktop-client/src/components/reports/spreadsheets/filterEmptyRows.ts b/packages/desktop-client/src/components/reports/spreadsheets/filterEmptyRows.ts
index 45efef476f4ed0af4fd70f0853f67170caf35dce..c0b6e8f3443f004dda1b964f276305bf1695fecb 100644
--- a/packages/desktop-client/src/components/reports/spreadsheets/filterEmptyRows.ts
+++ b/packages/desktop-client/src/components/reports/spreadsheets/filterEmptyRows.ts
@@ -1,5 +1,5 @@
 // @ts-strict-ignore
-import { type GroupedEntity } from 'loot-core/types/models/reports';
+import { type GroupedEntity } from 'loot-core/src/types/models/reports';
 
 export function filterEmptyRows(
   showEmpty: boolean,
diff --git a/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts
index 0adc9b119360ad34aebe30a993632c3243f4158b..f44cddf8ca43d4ec9f0ea943abf56741580f1b06 100644
--- a/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts
+++ b/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts
@@ -3,7 +3,7 @@ import { 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';
 import { integerToAmount } from 'loot-core/src/shared/util';
-import { type GroupedEntity } from 'loot-core/types/models/reports';
+import { type GroupedEntity } from 'loot-core/src/types/models/reports';
 
 import { categoryLists } from '../ReportOptions';
 
diff --git a/packages/desktop-client/src/components/rules/ScheduleValue.tsx b/packages/desktop-client/src/components/rules/ScheduleValue.tsx
index 036d0a2ed37226ee83fb0e12c740a04a23331771..fb05b0b8f2dddbed526705c1e75a191298d1defa 100644
--- a/packages/desktop-client/src/components/rules/ScheduleValue.tsx
+++ b/packages/desktop-client/src/components/rules/ScheduleValue.tsx
@@ -1,12 +1,11 @@
 import React from 'react';
-import { useSelector } from 'react-redux';
 
-import { type State } from 'loot-core/client/state-types';
-import { type QueriesState } from 'loot-core/client/state-types/queries';
 import { getPayeesById } from 'loot-core/src/client/reducers/queries';
 import { describeSchedule } from 'loot-core/src/shared/schedules';
 import { type ScheduleEntity } from 'loot-core/src/types/models';
 
+import { usePayees } from '../../hooks/usePayees';
+
 import { SchedulesQuery } from './SchedulesQuery';
 import { Value } from './Value';
 
@@ -15,9 +14,7 @@ type ScheduleValueProps = {
 };
 
 export function ScheduleValue({ value }: ScheduleValueProps) {
-  const payees = useSelector<State, QueriesState['payees']>(
-    state => state.queries.payees,
-  );
+  const payees = usePayees();
   const byId = getPayeesById(payees);
   const { data: schedules } = SchedulesQuery.useQuery();
 
diff --git a/packages/desktop-client/src/components/rules/Value.tsx b/packages/desktop-client/src/components/rules/Value.tsx
index fe9ccc73c2f701a6349a2ddef9b16ba0cbb319ae..d8b184b39d5c1c2ae6a0a6071224ea459c153c69 100644
--- a/packages/desktop-client/src/components/rules/Value.tsx
+++ b/packages/desktop-client/src/components/rules/Value.tsx
@@ -1,17 +1,16 @@
 // @ts-strict-ignore
 import React, { useState } from 'react';
-import { useSelector } from 'react-redux';
 
 import { format as formatDate, parseISO } from 'date-fns';
 
-import { type State } from 'loot-core/client/state-types';
-import { type PrefsState } from 'loot-core/client/state-types/prefs';
-import { type QueriesState } from 'loot-core/client/state-types/queries';
 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 { useAccounts } from '../../hooks/useAccounts';
 import { useCategories } from '../../hooks/useCategories';
+import { useDateFormat } from '../../hooks/useDateFormat';
+import { usePayees } from '../../hooks/usePayees';
 import { type CSSProperties, theme } from '../../style';
 import { LinkButton } from '../common/LinkButton';
 import { Text } from '../common/Text';
@@ -36,16 +35,10 @@ export function Value<T>({
   describe = x => x.name,
   style,
 }: ValueProps<T>) {
-  const dateFormat = useSelector<State, PrefsState['local']['dateFormat']>(
-    state => state.prefs.local.dateFormat || 'MM/dd/yyyy',
-  );
-  const payees = useSelector<State, QueriesState['payees']>(
-    state => state.queries.payees,
-  );
+  const dateFormat = useDateFormat() || 'MM/dd/yyyy';
+  const payees = usePayees();
   const { list: categories } = useCategories();
-  const accounts = useSelector<State, QueriesState['accounts']>(
-    state => state.queries.accounts,
-  );
+  const accounts = useAccounts();
   const valueStyle = {
     color: theme.pageTextPositive,
     ...style,
diff --git a/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx b/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx
index 03a0aebaf00326a14a5df228f6aa0a31786d9c81..1f62f40c8f439936a2324310caff0dd28ce67743 100644
--- a/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx
+++ b/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx
@@ -1,9 +1,6 @@
 // @ts-strict-ignore
 import React, { useState } from 'react';
-import { useSelector } from 'react-redux';
 
-import { type State } from 'loot-core/client/state-types';
-import { type PrefsState } from 'loot-core/client/state-types/prefs';
 import { runQuery } from 'loot-core/src/client/query-helpers';
 import { send } from 'loot-core/src/platform/client/fetch';
 import { q } from 'loot-core/src/shared/query';
@@ -11,6 +8,7 @@ import { getRecurringDescription } from 'loot-core/src/shared/schedules';
 import type { DiscoverScheduleEntity } from 'loot-core/src/types/models';
 
 import type { BoundActions } from '../../hooks/useActions';
+import { useDateFormat } from '../../hooks/useDateFormat';
 import {
   useSelected,
   useSelectedDispatch,
@@ -41,9 +39,7 @@ function DiscoverSchedulesTable({
 }) {
   const selectedItems = useSelectedItems();
   const dispatchSelected = useSelectedDispatch();
-  const dateFormat = useSelector<State, PrefsState['local']['dateFormat']>(
-    state => state.prefs.local.dateFormat || 'MM/dd/yyyy',
-  );
+  const dateFormat = useDateFormat() || 'MM/dd/yyyy';
 
   function renderItem({ item }: { item: DiscoverScheduleEntity }) {
     const selected = selectedItems.has(item.id);
diff --git a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx
index 85446055b9221f805b15c943062167216b62b984..56d20b4426521b38a064be9e3fe44a3620c9ecac 100644
--- a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx
+++ b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx
@@ -1,14 +1,15 @@
 import React, { useEffect, useReducer } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { useDispatch } from 'react-redux';
 
 import { pushModal } from 'loot-core/src/client/actions/modals';
-import { useCachedPayees } from 'loot-core/src/client/data-hooks/payees';
 import { runQuery, liveQuery } from 'loot-core/src/client/query-helpers';
 import { send, sendCatch } from 'loot-core/src/platform/client/fetch';
 import * as monthUtils from 'loot-core/src/shared/months';
 import { q } from 'loot-core/src/shared/query';
 import { extractScheduleConds } from 'loot-core/src/shared/schedules';
 
+import { useDateFormat } from '../../hooks/useDateFormat';
+import { usePayees } from '../../hooks/usePayees';
 import { useSelected, SelectedProvider } from '../../hooks/useSelected';
 import { theme } from '../../style';
 import { AccountAutocomplete } from '../autocomplete/AccountAutocomplete';
@@ -70,11 +71,10 @@ function updateScheduleConditions(schedule, fields) {
 export function ScheduleDetails({ modalProps, actions, id, transaction }) {
   const adding = id == null;
   const fromTrans = transaction != null;
-  const payees = useCachedPayees({ idKey: true });
+  const payees = usePayees({ idKey: true });
   const globalDispatch = useDispatch();
-  const dateFormat = useSelector(state => {
-    return state.prefs.local.dateFormat || 'MM/dd/yyyy';
-  });
+  const dateFormat = useDateFormat() || 'MM/dd/yyyy';
+
   const [state, dispatch] = useReducer(
     (state, action) => {
       switch (action.type) {
diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx
index b2ec6ea021c717a2fa34d90b6815e1b3b10bc273..ba34e2df104192b6dd2d7e839553d446408f910c 100644
--- a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx
+++ b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx
@@ -1,11 +1,6 @@
 // @ts-strict-ignore
 import React, { useState, useMemo, type CSSProperties } from 'react';
-import { useSelector } from 'react-redux';
 
-import { type State } from 'loot-core/client/state-types';
-import { type PrefsState } from 'loot-core/client/state-types/prefs';
-import { useCachedAccounts } from 'loot-core/src/client/data-hooks/accounts';
-import { useCachedPayees } from 'loot-core/src/client/data-hooks/payees';
 import {
   type ScheduleStatusType,
   type ScheduleStatuses,
@@ -15,6 +10,9 @@ import { getScheduledAmount } from 'loot-core/src/shared/schedules';
 import { integerToCurrency } from 'loot-core/src/shared/util';
 import { type ScheduleEntity } from 'loot-core/src/types/models';
 
+import { useAccounts } from '../../hooks/useAccounts';
+import { useDateFormat } from '../../hooks/useDateFormat';
+import { usePayees } from '../../hooks/usePayees';
 import { SvgDotsHorizontalTriple } from '../../icons/v1';
 import { SvgCheck } from '../../icons/v2';
 import { theme } from '../../style';
@@ -196,16 +194,11 @@ export function SchedulesTable({
   onAction,
   tableStyle,
 }: SchedulesTableProps) {
-  const dateFormat = useSelector<State, PrefsState['local']['dateFormat']>(
-    state => {
-      return state.prefs.local.dateFormat || 'MM/dd/yyyy';
-    },
-  );
-
+  const dateFormat = useDateFormat() || 'MM/dd/yyyy';
   const [showCompleted, setShowCompleted] = useState(false);
 
-  const payees = useCachedPayees();
-  const accounts = useCachedAccounts();
+  const payees = usePayees();
+  const accounts = useAccounts();
 
   const filteredSchedules = useMemo(() => {
     if (!filter) {
@@ -240,7 +233,7 @@ export function SchedulesTable({
         filterIncludes(dateStr)
       );
     });
-  }, [schedules, filter, statuses]);
+  }, [payees, accounts, schedules, filter, statuses]);
 
   const items: SchedulesTableItem[] = useMemo(() => {
     const unCompletedSchedules = filteredSchedules.filter(s => !s.completed);
diff --git a/packages/desktop-client/src/components/select/DateSelect.tsx b/packages/desktop-client/src/components/select/DateSelect.tsx
index a6b83fc65f9f0ea4b05887ef07d4dc03e0b1dbb7..6893edc8d268089766094c6f166c6a65b6e3821f 100644
--- a/packages/desktop-client/src/components/select/DateSelect.tsx
+++ b/packages/desktop-client/src/components/select/DateSelect.tsx
@@ -10,15 +10,12 @@ import React, {
   type MutableRefObject,
   type KeyboardEvent,
 } from 'react';
-import { useSelector } from 'react-redux';
 
 import { parse, parseISO, format, subDays, addDays, isValid } from 'date-fns';
 import Pikaday from 'pikaday';
 
 import 'pikaday/css/pikaday.css';
 
-import { type State } from 'loot-core/client/state-types';
-import { type PrefsState } from 'loot-core/client/state-types/prefs';
 import {
   getDayMonthFormat,
   getDayMonthRegex,
@@ -28,6 +25,7 @@ import {
 } from 'loot-core/src/shared/months';
 import { stringToInteger } from 'loot-core/src/shared/util';
 
+import { useLocalPref } from '../../hooks/useLocalPref';
 import { type CSSProperties, theme } from '../../style';
 import { Input, type InputProps } from '../common/Input';
 import { View, type ViewProps } from '../common/View';
@@ -233,14 +231,8 @@ export function DateSelect({
   const [selectedValue, setSelectedValue] = useState(value);
   const userSelectedValue = useRef(selectedValue);
 
-  const firstDayOfWeekIdx = useSelector<
-    State,
-    PrefsState['local']['firstDayOfWeekIdx']
-  >(state =>
-    state.prefs.local?.firstDayOfWeekIdx
-      ? state.prefs.local.firstDayOfWeekIdx
-      : '0',
-  );
+  const [_firstDayOfWeekIdx] = useLocalPref('firstDayOfWeekIdx');
+  const firstDayOfWeekIdx = _firstDayOfWeekIdx || '0';
 
   useEffect(() => {
     userSelectedValue.current = value;
diff --git a/packages/desktop-client/src/components/select/RecurringSchedulePicker.jsx b/packages/desktop-client/src/components/select/RecurringSchedulePicker.jsx
index df10237168f60ab9f610fb74661b11ca4ba174af..17ee82e87c29c5de3077183eaa75c7d580a9824e 100644
--- a/packages/desktop-client/src/components/select/RecurringSchedulePicker.jsx
+++ b/packages/desktop-client/src/components/select/RecurringSchedulePicker.jsx
@@ -1,10 +1,10 @@
 import React, { useEffect, useReducer, useState } from 'react';
-import { useSelector } from 'react-redux';
 
 import { sendCatch } from 'loot-core/src/platform/client/fetch';
 import * as monthUtils from 'loot-core/src/shared/months';
 import { getRecurringDescription } from 'loot-core/src/shared/schedules';
 
+import { useDateFormat } from '../../hooks/useDateFormat';
 import { SvgAdd, SvgSubtract } from '../../icons/v0';
 import { theme } from '../../style';
 import { Button } from '../common/Button';
@@ -159,11 +159,9 @@ function reducer(state, action) {
 }
 
 function SchedulePreview({ previewDates }) {
-  const dateFormat = useSelector(state =>
-    (state.prefs.local.dateFormat || 'MM/dd/yyyy')
-      .replace('MM', 'M')
-      .replace('dd', 'd'),
-  );
+  const dateFormat = (useDateFormat() || 'MM/dd/yyyy')
+    .replace('MM', 'M')
+    .replace('dd', 'd');
 
   if (!previewDates) {
     return null;
@@ -281,9 +279,7 @@ function RecurringScheduleTooltip({ config: currentConfig, onClose, onSave }) {
   const skipWeekend = state.config.hasOwnProperty('skipWeekend')
     ? state.config.skipWeekend
     : false;
-  const dateFormat = useSelector(
-    state => state.prefs.local.dateFormat || 'MM/dd/yyyy',
-  );
+  const dateFormat = useDateFormat() || 'MM/dd/yyyy';
 
   useEffect(() => {
     dispatch({
@@ -481,9 +477,7 @@ function RecurringScheduleTooltip({ config: currentConfig, onClose, onSave }) {
 
 export function RecurringSchedulePicker({ value, buttonStyle, onChange }) {
   const { isOpen, close, getOpenEvents } = useTooltip();
-  const dateFormat = useSelector(
-    state => state.prefs.local.dateFormat || 'MM/dd/yyyy',
-  );
+  const dateFormat = useDateFormat() || 'MM/dd/yyyy';
 
   function onSave(config) {
     onChange(config);
diff --git a/packages/desktop-client/src/components/settings/Encryption.tsx b/packages/desktop-client/src/components/settings/Encryption.tsx
index cfe994b13a3b62d2c74275939f086181cc3b14d1..97a7660ce2f6476ef162e8f19044de5a9a04de39 100644
--- a/packages/desktop-client/src/components/settings/Encryption.tsx
+++ b/packages/desktop-client/src/components/settings/Encryption.tsx
@@ -1,11 +1,8 @@
 // @ts-strict-ignore
 import React from 'react';
-import { useSelector } from 'react-redux';
-
-import { type State } from 'loot-core/client/state-types';
-import { type PrefsState } from 'loot-core/client/state-types/prefs';
 
 import { useActions } from '../../hooks/useActions';
+import { useLocalPref } from '../../hooks/useLocalPref';
 import { theme } from '../../style';
 import { Button } from '../common/Button';
 import { ExternalLink } from '../common/ExternalLink';
@@ -17,9 +14,7 @@ import { Setting } from './UI';
 export function EncryptionSettings() {
   const { pushModal } = useActions();
   const serverURL = useServerURL();
-  const encryptKeyId = useSelector<State, PrefsState['local']['encryptKeyId']>(
-    state => state.prefs.local.encryptKeyId,
-  );
+  const [encryptKeyId] = useLocalPref('encryptKeyId');
 
   const missingCryptoAPI = !(window.crypto && crypto.subtle);
 
diff --git a/packages/desktop-client/src/components/settings/Experimental.tsx b/packages/desktop-client/src/components/settings/Experimental.tsx
index fc1a942c521c571646de45065112327c9575b618..15f20989fecf4cbf98cb8bf2707eac438bbb7912 100644
--- a/packages/desktop-client/src/components/settings/Experimental.tsx
+++ b/packages/desktop-client/src/components/settings/Experimental.tsx
@@ -1,12 +1,9 @@
 import { type ReactNode, useState } from 'react';
-import { useSelector } from 'react-redux';
 
-import { type State } from 'loot-core/src/client/state-types';
-import { type PrefsState } from 'loot-core/src/client/state-types/prefs';
 import type { FeatureFlag } from 'loot-core/src/types/prefs';
 
-import { useActions } from '../../hooks/useActions';
 import { useFeatureFlag } from '../../hooks/useFeatureFlag';
+import { useLocalPref } from '../../hooks/useLocalPref';
 import { theme } from '../../style';
 import { LinkButton } from '../common/LinkButton';
 import { Text } from '../common/Text';
@@ -23,23 +20,20 @@ type FeatureToggleProps = {
 };
 
 function FeatureToggle({
-  flag,
+  flag: flagName,
   disableToggle = false,
   error,
   children,
 }: FeatureToggleProps) {
-  const { savePrefs } = useActions();
-  const enabled = useFeatureFlag(flag);
+  const enabled = useFeatureFlag(flagName);
+  const [_, setFlagPref] = useLocalPref(`flags.${flagName}`);
 
   return (
     <label style={{ display: 'flex' }}>
       <Checkbox
         checked={enabled}
         onChange={() => {
-          // @ts-expect-error key type is not correctly inferred
-          savePrefs({
-            [`flags.${flag}`]: !enabled,
-          });
+          setFlagPref(!enabled);
         }}
         disabled={disableToggle}
       />
@@ -63,9 +57,7 @@ function FeatureToggle({
 }
 
 function ReportBudgetFeature() {
-  const budgetType = useSelector<State, PrefsState['local']['budgetType']>(
-    state => state.prefs.local?.budgetType,
-  );
+  const [budgetType] = useLocalPref('budgetType');
   const enabled = useFeatureFlag('reportBudget');
   const blockToggleOff = budgetType === 'report' && enabled;
   return (
diff --git a/packages/desktop-client/src/components/settings/Export.tsx b/packages/desktop-client/src/components/settings/Export.tsx
index 6cd2e7449894ab378ea4c30bb0f75359a3ef6c96..824c0f4fa41b7f96f6d7284f05a435b9d9b8afb3 100644
--- a/packages/desktop-client/src/components/settings/Export.tsx
+++ b/packages/desktop-client/src/components/settings/Export.tsx
@@ -1,13 +1,11 @@
 // @ts-strict-ignore
 import React, { useState } from 'react';
-import { useSelector } from 'react-redux';
 
 import { format } from 'date-fns';
 
-import { type State } from 'loot-core/client/state-types';
-import { type PrefsState } from 'loot-core/client/state-types/prefs';
 import { send } from 'loot-core/src/platform/client/fetch';
 
+import { useLocalPref } from '../../hooks/useLocalPref';
 import { theme } from '../../style';
 import { Block } from '../common/Block';
 import { ButtonWithLoading } from '../common/Button';
@@ -18,12 +16,8 @@ import { Setting } from './UI';
 export function ExportBudget() {
   const [isLoading, setIsLoading] = useState(false);
   const [error, setError] = useState<string | null>(null);
-  const budgetId = useSelector<State, PrefsState['local']['id']>(
-    state => state.prefs.local.id,
-  );
-  const encryptKeyId = useSelector<State, PrefsState['local']['encryptKeyId']>(
-    state => state.prefs.local.encryptKeyId,
-  );
+  const [budgetId] = useLocalPref('id');
+  const [encryptKeyId] = useLocalPref('encryptKeyId');
 
   async function onExport() {
     setIsLoading(true);
diff --git a/packages/desktop-client/src/components/settings/Format.tsx b/packages/desktop-client/src/components/settings/Format.tsx
index 8f36cc434b331f37f75cf2731b5239b6e1f52ec7..0dc2fbf0387c431b2e7d9a6e244336f49ca87992 100644
--- a/packages/desktop-client/src/components/settings/Format.tsx
+++ b/packages/desktop-client/src/components/settings/Format.tsx
@@ -1,20 +1,18 @@
 // @ts-strict-ignore
 import React, { type ReactNode } from 'react';
-import { useSelector } from 'react-redux';
 
-import { type State } from 'loot-core/client/state-types';
-import { type PrefsState } from 'loot-core/client/state-types/prefs';
 import { numberFormats } from 'loot-core/src/shared/util';
 import { type LocalPrefs } from 'loot-core/src/types/prefs';
 
-import { useActions } from '../../hooks/useActions';
+import { useDateFormat } from '../../hooks/useDateFormat';
+import { useLocalPref } from '../../hooks/useLocalPref';
 import { tokens } from '../../tokens';
 import { Button } from '../common/Button';
 import { Select } from '../common/Select';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
 import { Checkbox } from '../forms';
-import { useSidebar } from '../sidebar';
+import { useSidebar } from '../sidebar/SidebarProvider';
 
 import { Setting } from './UI';
 
@@ -56,24 +54,16 @@ function Column({ title, children }: { title: string; children: ReactNode }) {
 }
 
 export function FormatSettings() {
-  const { savePrefs } = useActions();
-
   const sidebar = useSidebar();
-  const firstDayOfWeekIdx = useSelector<
-    State,
-    PrefsState['local']['firstDayOfWeekIdx']
-  >(
-    state => state.prefs.local.firstDayOfWeekIdx || '0', // Sunday
-  );
-  const dateFormat = useSelector<State, PrefsState['local']['dateFormat']>(
-    state => state.prefs.local.dateFormat || 'MM/dd/yyyy',
-  );
-  const numberFormat = useSelector<State, PrefsState['local']['numberFormat']>(
-    state => state.prefs.local.numberFormat || 'comma-dot',
-  );
-  const hideFraction = useSelector<State, PrefsState['local']['hideFraction']>(
-    state => state.prefs.local.hideFraction,
-  );
+  const [_firstDayOfWeekIdx, setFirstDayOfWeekIdxPref] =
+    useLocalPref('firstDayOfWeekIdx'); // Sunday;
+  const firstDayOfWeekIdx = _firstDayOfWeekIdx || '0';
+  const dateFormat = useDateFormat() || 'MM/dd/yyyy';
+  const [, setDateFormatPref] = useLocalPref('dateFormat');
+  const [_numberFormat, setNumberFormatPref] = useLocalPref('numberFormat');
+  const numberFormat = _numberFormat || 'comma-dot';
+  const [hideFraction = false, setHideFractionPref] =
+    useLocalPref('hideFraction');
 
   return (
     <Setting
@@ -98,7 +88,7 @@ export function FormatSettings() {
                 bare
                 key={String(hideFraction)} // needed because label does not update
                 value={numberFormat}
-                onChange={format => savePrefs({ numberFormat: format })}
+                onChange={format => setNumberFormatPref(format)}
                 options={numberFormats.map(f => [
                   f.value,
                   hideFraction ? f.labelNoFraction : f.label,
@@ -111,9 +101,7 @@ export function FormatSettings() {
               <Checkbox
                 id="settings-textDecimal"
                 checked={!!hideFraction}
-                onChange={e =>
-                  savePrefs({ hideFraction: e.currentTarget.checked })
-                }
+                onChange={e => setHideFractionPref(e.currentTarget.checked)}
               />
               <label htmlFor="settings-textDecimal">Hide decimal places</label>
             </Text>
@@ -124,7 +112,7 @@ export function FormatSettings() {
               <Select
                 bare
                 value={dateFormat}
-                onChange={format => savePrefs({ dateFormat: format })}
+                onChange={format => setDateFormatPref(format)}
                 options={dateFormats.map(f => [f.value, f.label])}
                 style={{ padding: '2px 10px', fontSize: 15 }}
               />
@@ -136,7 +124,7 @@ export function FormatSettings() {
               <Select
                 bare
                 value={firstDayOfWeekIdx}
-                onChange={idx => savePrefs({ firstDayOfWeekIdx: idx })}
+                onChange={idx => setFirstDayOfWeekIdxPref(idx)}
                 options={daysOfWeek.map(f => [f.value, f.label])}
                 style={{ padding: '2px 10px', fontSize: 15 }}
               />
diff --git a/packages/desktop-client/src/components/settings/Global.tsx b/packages/desktop-client/src/components/settings/Global.tsx
index aad153e5ebb9df6cd94b2fbe4d3569c2b9d72034..92e4ba843a871fbf15cad60f2940fbfea4ddeb4c 100644
--- a/packages/desktop-client/src/components/settings/Global.tsx
+++ b/packages/desktop-client/src/components/settings/Global.tsx
@@ -1,11 +1,7 @@
 // @ts-strict-ignore
 import React, { useState, useEffect, useRef } from 'react';
-import { useSelector } from 'react-redux';
 
-import { type State } from 'loot-core/client/state-types';
-import { type PrefsState } from 'loot-core/client/state-types/prefs';
-
-import { useActions } from '../../hooks/useActions';
+import { useGlobalPref } from '../../hooks/useGlobalPref';
 import { theme } from '../../style';
 import { Information } from '../alerts';
 import { Button } from '../common/Button';
@@ -15,10 +11,7 @@ import { View } from '../common/View';
 import { Setting } from './UI';
 
 export function GlobalSettings() {
-  const documentDir = useSelector<State, PrefsState['global']['documentDir']>(
-    state => state.prefs.global.documentDir,
-  );
-  const { saveGlobalPrefs } = useActions();
+  const [documentDir, setDocumentDirPref] = useGlobalPref('documentDir');
 
   const [documentDirChanged, setDirChanged] = useState(false);
   const dirScrolled = useRef<HTMLSpanElement>(null);
@@ -34,7 +27,7 @@ export function GlobalSettings() {
       properties: ['openDirectory'],
     });
     if (res) {
-      saveGlobalPrefs({ documentDir: res[0] });
+      setDocumentDirPref(res[0]);
       setDirChanged(true);
     }
   }
diff --git a/packages/desktop-client/src/components/settings/Reset.tsx b/packages/desktop-client/src/components/settings/Reset.tsx
index 744ada38e2b6b01cbacfd65ac86183959916604b..0e8ea3cea5a2fd1cf97441fd8dd25b2fb051af0f 100644
--- a/packages/desktop-client/src/components/settings/Reset.tsx
+++ b/packages/desktop-client/src/components/settings/Reset.tsx
@@ -1,12 +1,10 @@
 // @ts-strict-ignore
 import React, { useState } from 'react';
-import { useSelector } from 'react-redux';
 
-import { type State } from 'loot-core/client/state-types';
-import { type PrefsState } from 'loot-core/client/state-types/prefs';
 import { send } from 'loot-core/src/platform/client/fetch';
 
 import { useActions } from '../../hooks/useActions';
+import { useLocalPref } from '../../hooks/useLocalPref';
 import { ButtonWithLoading } from '../common/Button';
 import { Text } from '../common/Text';
 
@@ -41,9 +39,8 @@ export function ResetCache() {
 }
 
 export function ResetSync() {
-  const isEnabled = !!useSelector<State, PrefsState['local']['groupId']>(
-    state => state.prefs.local.groupId,
-  );
+  const [groupId] = useLocalPref('groupId');
+  const isEnabled = !!groupId;
   const { resetSync } = useActions();
 
   const [resetting, setResetting] = useState(false);
diff --git a/packages/desktop-client/src/components/settings/Themes.tsx b/packages/desktop-client/src/components/settings/Themes.tsx
index 775ef52534090ff5fe31b076f6a0298180653dad..7966b546a33d2d7c4811d7f7f7b587bf24fc0147 100644
--- a/packages/desktop-client/src/components/settings/Themes.tsx
+++ b/packages/desktop-client/src/components/settings/Themes.tsx
@@ -1,6 +1,7 @@
 import React from 'react';
 
-import { useActions } from '../../hooks/useActions';
+import { type Theme } from 'loot-core/types/prefs';
+
 import { themeOptions, useTheme } from '../../style';
 import { Button } from '../common/Button';
 import { Select } from '../common/Select';
@@ -9,17 +10,16 @@ import { Text } from '../common/Text';
 import { Setting } from './UI';
 
 export function ThemeSettings() {
-  const theme = useTheme();
-  const { saveGlobalPrefs } = useActions();
+  const [theme, switchTheme] = useTheme();
 
   return (
     <Setting
       primaryAction={
         <Button bounce={false} style={{ padding: 0 }}>
-          <Select
+          <Select<Theme>
             bare
             onChange={value => {
-              saveGlobalPrefs({ theme: value });
+              switchTheme(value);
             }}
             value={theme}
             options={themeOptions}
diff --git a/packages/desktop-client/src/components/settings/index.tsx b/packages/desktop-client/src/components/settings/index.tsx
index 454f415756cba0657e7e53e37505254d60ecd74d..2f7ef5ada70ced36938eda5739c96e19f1dd257e 100644
--- a/packages/desktop-client/src/components/settings/index.tsx
+++ b/packages/desktop-client/src/components/settings/index.tsx
@@ -1,16 +1,15 @@
 // @ts-strict-ignore
 import React, { type ReactNode, useEffect } from 'react';
-import { useSelector } from 'react-redux';
 
 import { media } from 'glamor';
 
-import { type State } from 'loot-core/client/state-types';
-import { type PrefsState } from 'loot-core/client/state-types/prefs';
 import * as Platform from 'loot-core/src/client/platform';
 import { listen } from 'loot-core/src/platform/client/fetch';
 
 import { useActions } from '../../hooks/useActions';
+import { useGlobalPref } from '../../hooks/useGlobalPref';
 import { useLatestVersion, useIsOutdated } from '../../hooks/useLatestVersion';
+import { useLocalPref } from '../../hooks/useLocalPref';
 import { useSetThemeColor } from '../../hooks/useSetThemeColor';
 import { useResponsive } from '../../ResponsiveProvider';
 import { theme } from '../../style';
@@ -91,12 +90,8 @@ function IDName({ children }: { children: ReactNode }) {
 }
 
 function AdvancedAbout() {
-  const budgetId = useSelector<State, PrefsState['local']['id']>(
-    state => state.prefs.local.id,
-  );
-  const groupId = useSelector<State, PrefsState['local']['groupId']>(
-    state => state.prefs.local.groupId,
-  );
+  const [budgetId] = useLocalPref('id');
+  const [groupId] = useLocalPref('groupId');
 
   return (
     <Setting>
@@ -124,13 +119,8 @@ function AdvancedAbout() {
 }
 
 export function Settings() {
-  const floatingSidebar = useSelector<
-    State,
-    PrefsState['global']['floatingSidebar']
-  >(state => state.prefs.global.floatingSidebar);
-  const budgetName = useSelector<State, PrefsState['local']['budgetName']>(
-    state => state.prefs.local.budgetName,
-  );
+  const [floatingSidebar] = useGlobalPref('floatingSidebar');
+  const [budgetName] = useLocalPref('budgetName');
 
   const { loadPrefs, closeBudget } = useActions();
 
diff --git a/packages/desktop-client/src/components/sidebar/Accounts.tsx b/packages/desktop-client/src/components/sidebar/Accounts.tsx
index cd20ccb58562f69b6de0aef7e9d12e0bb78ad762..e85e32e4a193508811955b8527b38d6e45988bc6 100644
--- a/packages/desktop-client/src/components/sidebar/Accounts.tsx
+++ b/packages/desktop-client/src/components/sidebar/Accounts.tsx
@@ -1,12 +1,17 @@
 // @ts-strict-ignore
-import React, { useState, useMemo } from 'react';
+import React, { useState } from 'react';
 
-import { type AccountEntity } from 'loot-core/src/types/models';
+import * as queries from 'loot-core/src/client/queries';
 
+import { useBudgetedAccounts } from '../../hooks/useBudgetedAccounts';
+import { useClosedAccounts } from '../../hooks/useClosedAccounts';
+import { useFailedAccounts } from '../../hooks/useFailedAccounts';
+import { useLocalPref } from '../../hooks/useLocalPref';
+import { useOffBudgetAccounts } from '../../hooks/useOffBudgetAccounts';
+import { useUpdatedAccounts } from '../../hooks/useUpdatedAccounts';
 import { SvgAdd } from '../../icons/v1';
 import { View } from '../common/View';
 import { type OnDropCallback } from '../sort';
-import { type Binding } from '../spreadsheet';
 
 import { Account } from './Account';
 import { SecondaryItem } from './SecondaryItem';
@@ -14,65 +19,26 @@ import { SecondaryItem } from './SecondaryItem';
 const fontWeight = 600;
 
 type AccountsProps = {
-  accounts: AccountEntity[];
-  failedAccounts: Map<
-    string,
-    {
-      type: string;
-      code: string;
-    }
-  >;
-  updatedAccounts: string[];
-  getAccountPath: (account: AccountEntity) => string;
-  allAccountsPath: string;
-  budgetedAccountPath: string;
-  offBudgetAccountPath: string;
-  getBalanceQuery: (account: AccountEntity) => Binding;
-  getAllAccountBalance: () => Binding;
-  getOnBudgetBalance: () => Binding;
-  getOffBudgetBalance: () => Binding;
-  showClosedAccounts: boolean;
   onAddAccount: () => void;
   onToggleClosedAccounts: () => void;
   onReorder: OnDropCallback;
 };
 
 export function Accounts({
-  accounts,
-  failedAccounts,
-  updatedAccounts,
-  getAccountPath,
-  allAccountsPath,
-  budgetedAccountPath,
-  offBudgetAccountPath,
-  getBalanceQuery,
-  getAllAccountBalance,
-  getOnBudgetBalance,
-  getOffBudgetBalance,
-  showClosedAccounts,
   onAddAccount,
   onToggleClosedAccounts,
   onReorder,
 }: AccountsProps) {
   const [isDragging, setIsDragging] = useState(false);
-  const offbudgetAccounts = useMemo(
-    () =>
-      accounts.filter(
-        account => account.closed === 0 && account.offbudget === 1,
-      ),
-    [accounts],
-  );
-  const budgetedAccounts = useMemo(
-    () =>
-      accounts.filter(
-        account => account.closed === 0 && account.offbudget === 0,
-      ),
-    [accounts],
-  );
-  const closedAccounts = useMemo(
-    () => accounts.filter(account => account.closed === 1),
-    [accounts],
-  );
+  const failedAccounts = useFailedAccounts();
+  const updatedAccounts = useUpdatedAccounts();
+  const offbudgetAccounts = useOffBudgetAccounts();
+  const budgetedAccounts = useBudgetedAccounts();
+  const closedAccounts = useClosedAccounts();
+
+  const getAccountPath = account => `/accounts/${account.id}`;
+
+  const [showClosedAccounts] = useLocalPref('ui.showClosedAccounts');
 
   function onDragChange(drag) {
     setIsDragging(drag.state === 'start');
@@ -92,16 +58,16 @@ export function Accounts({
     <View>
       <Account
         name="All accounts"
-        to={allAccountsPath}
-        query={getAllAccountBalance()}
+        to="/accounts"
+        query={queries.allAccountBalance()}
         style={{ fontWeight, marginTop: 15 }}
       />
 
       {budgetedAccounts.length > 0 && (
         <Account
           name="For budget"
-          to={budgetedAccountPath}
-          query={getOnBudgetBalance()}
+          to="/accounts/budgeted"
+          query={queries.budgetedAccountBalance()}
           style={{ fontWeight, marginTop: 13 }}
         />
       )}
@@ -115,7 +81,7 @@ export function Accounts({
           failed={failedAccounts && failedAccounts.has(account.id)}
           updated={updatedAccounts && updatedAccounts.includes(account.id)}
           to={getAccountPath(account)}
-          query={getBalanceQuery(account)}
+          query={queries.accountBalance(account)}
           onDragChange={onDragChange}
           onDrop={onReorder}
           outerStyle={makeDropPadding(i)}
@@ -125,8 +91,8 @@ export function Accounts({
       {offbudgetAccounts.length > 0 && (
         <Account
           name="Off budget"
-          to={offBudgetAccountPath}
-          query={getOffBudgetBalance()}
+          to="/accounts/offbudget"
+          query={queries.offbudgetAccountBalance()}
           style={{ fontWeight, marginTop: 13 }}
         />
       )}
@@ -140,7 +106,7 @@ export function Accounts({
           failed={failedAccounts && failedAccounts.has(account.id)}
           updated={updatedAccounts && updatedAccounts.includes(account.id)}
           to={getAccountPath(account)}
-          query={getBalanceQuery(account)}
+          query={queries.accountBalance(account)}
           onDragChange={onDragChange}
           onDrop={onReorder}
           outerStyle={makeDropPadding(i)}
@@ -163,7 +129,7 @@ export function Accounts({
             name={account.name}
             account={account}
             to={getAccountPath(account)}
-            query={getBalanceQuery(account)}
+            query={queries.accountBalance(account)}
             onDragChange={onDragChange}
             onDrop={onReorder}
           />
diff --git a/packages/desktop-client/src/components/sidebar/Sidebar.tsx b/packages/desktop-client/src/components/sidebar/Sidebar.tsx
index dadd3ba95fcab62f03300eb10afc3b917ff831ac..b8d59bbdec60b9c887dbceeb5ddfcb99f5bff3c9 100644
--- a/packages/desktop-client/src/components/sidebar/Sidebar.tsx
+++ b/packages/desktop-client/src/components/sidebar/Sidebar.tsx
@@ -1,68 +1,74 @@
-import React, { type ReactNode } from 'react';
-
+import React, { useState } from 'react';
+import { useDispatch } from 'react-redux';
+
+import {
+  closeBudget,
+  moveAccount,
+  replaceModal,
+} from 'loot-core/src/client/actions';
 import * as Platform from 'loot-core/src/client/platform';
-import { type AccountEntity } from 'loot-core/src/types/models';
 
+import { useAccounts } from '../../hooks/useAccounts';
+import { useGlobalPref } from '../../hooks/useGlobalPref';
+import { useLocalPref } from '../../hooks/useLocalPref';
+import { useNavigate } from '../../hooks/useNavigate';
+import { SvgExpandArrow } from '../../icons/v0';
 import { SvgReports, SvgWallet } from '../../icons/v1';
 import { SvgCalendar } from '../../icons/v2';
-import { type CSSProperties, theme } from '../../style';
+import { styles, theme } from '../../style';
+import { Button } from '../common/Button';
+import { InitialFocus } from '../common/InitialFocus';
+import { Input } from '../common/Input';
+import { Menu } from '../common/Menu';
+import { Text } from '../common/Text';
 import { View } from '../common/View';
-import { type OnDropCallback } from '../sort';
-import { type Binding } from '../spreadsheet';
+import { Tooltip } from '../tooltips';
 
 import { Accounts } from './Accounts';
 import { Item } from './Item';
+import { useSidebar } from './SidebarProvider';
 import { ToggleButton } from './ToggleButton';
 import { Tools } from './Tools';
 
-import { useSidebar } from '.';
-
 export const SIDEBAR_WIDTH = 240;
 
-type SidebarProps = {
-  style: CSSProperties;
-  budgetName: ReactNode;
-  accounts: AccountEntity[];
-  failedAccounts: Map<
-    string,
-    {
-      type: string;
-      code: string;
-    }
-  >;
-  updatedAccounts: string[];
-  getBalanceQuery: (account: AccountEntity) => Binding;
-  getAllAccountBalance: () => Binding;
-  getOnBudgetBalance: () => Binding;
-  getOffBudgetBalance: () => Binding;
-  showClosedAccounts: boolean;
-  isFloating: boolean;
-  onFloat: () => void;
-  onAddAccount: () => void;
-  onToggleClosedAccounts: () => void;
-  onReorder: OnDropCallback;
-};
-
-export function Sidebar({
-  style,
-  budgetName,
-  accounts,
-  failedAccounts,
-  updatedAccounts,
-  getBalanceQuery,
-  getAllAccountBalance,
-  getOnBudgetBalance,
-  getOffBudgetBalance,
-  showClosedAccounts,
-  isFloating,
-  onFloat,
-  onAddAccount,
-  onToggleClosedAccounts,
-  onReorder,
-}: SidebarProps) {
+export function Sidebar() {
   const hasWindowButtons = !Platform.isBrowser && Platform.OS === 'mac';
 
+  const dispatch = useDispatch();
   const sidebar = useSidebar();
+  const accounts = useAccounts();
+  const [showClosedAccounts, setShowClosedAccountsPref] = useLocalPref(
+    'ui.showClosedAccounts',
+  );
+  const [isFloating = false, setFloatingSidebarPref] =
+    useGlobalPref('floatingSidebar');
+
+  async function onReorder(
+    id: string,
+    dropPos: 'top' | 'bottom',
+    targetId: unknown,
+  ) {
+    let targetIdToMove = targetId;
+    if (dropPos === 'bottom') {
+      const idx = accounts.findIndex(a => a.id === targetId) + 1;
+      targetIdToMove = idx < accounts.length ? accounts[idx].id : null;
+    }
+
+    dispatch(moveAccount(id, targetIdToMove));
+  }
+
+  const onFloat = () => {
+    setFloatingSidebarPref(!isFloating);
+  };
+
+  const onAddAccount = () => {
+    dispatch(replaceModal('add-account'));
+  };
+
+  const onToggleClosedAccounts = () => {
+    setShowClosedAccountsPref(!showClosedAccounts);
+  };
 
   return (
     <View
@@ -79,7 +85,8 @@ export function Sidebar({
           opacity: 1,
           width: hasWindowButtons ? null : 'auto',
         },
-        ...style,
+        flex: 1,
+        ...styles.darkScrollbar,
       }}
     >
       <View
@@ -96,7 +103,7 @@ export function Sidebar({
           }),
         }}
       >
-        {budgetName}
+        <EditableBudgetName />
 
         <View style={{ flex: 1, flexDirection: 'row' }} />
 
@@ -123,18 +130,6 @@ export function Sidebar({
         />
 
         <Accounts
-          accounts={accounts}
-          failedAccounts={failedAccounts}
-          updatedAccounts={updatedAccounts}
-          getAccountPath={account => `/accounts/${account.id}`}
-          allAccountsPath="/accounts"
-          budgetedAccountPath="/accounts/budgeted"
-          offBudgetAccountPath="/accounts/offbudget"
-          getBalanceQuery={getBalanceQuery}
-          getAllAccountBalance={getAllAccountBalance}
-          getOnBudgetBalance={getOnBudgetBalance}
-          getOffBudgetBalance={getOffBudgetBalance}
-          showClosedAccounts={showClosedAccounts}
           onAddAccount={onAddAccount}
           onToggleClosedAccounts={onToggleClosedAccounts}
           onReorder={onReorder}
@@ -143,3 +138,90 @@ export function Sidebar({
     </View>
   );
 }
+
+function EditableBudgetName() {
+  const dispatch = useDispatch();
+  const navigate = useNavigate();
+  const [budgetName, setBudgetNamePref] = useLocalPref('budgetName');
+  const [editing, setEditing] = useState(false);
+  const [menuOpen, setMenuOpen] = useState(false);
+
+  function onMenuSelect(type: string) {
+    setMenuOpen(false);
+
+    switch (type) {
+      case 'rename':
+        setEditing(true);
+        break;
+      case 'settings':
+        navigate('/settings');
+        break;
+      case 'help':
+        window.open('https://actualbudget.org/docs/', '_blank');
+        break;
+      case 'close':
+        dispatch(closeBudget());
+        break;
+      default:
+    }
+  }
+
+  const items = [
+    { name: 'rename', text: 'Rename budget' },
+    { name: 'settings', text: 'Settings' },
+    ...(Platform.isBrowser ? [{ name: 'help', text: 'Help' }] : []),
+    { name: 'close', text: 'Close file' },
+  ];
+
+  if (editing) {
+    return (
+      <InitialFocus>
+        <Input
+          style={{
+            width: 160,
+            fontSize: 16,
+            fontWeight: 500,
+          }}
+          defaultValue={budgetName}
+          onEnter={async e => {
+            const inputEl = e.target as HTMLInputElement;
+            const newBudgetName = inputEl.value;
+            if (newBudgetName.trim() !== '') {
+              setBudgetNamePref(inputEl.name);
+              setEditing(false);
+            }
+          }}
+          onBlur={() => setEditing(false)}
+        />
+      </InitialFocus>
+    );
+  } else {
+    return (
+      <Button
+        type="bare"
+        color={theme.buttonNormalBorder}
+        style={{
+          fontSize: 16,
+          fontWeight: 500,
+          marginLeft: -5,
+          flex: '0 auto',
+        }}
+        onClick={() => setMenuOpen(true)}
+      >
+        <Text style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
+          {budgetName || 'A budget has no name'}
+        </Text>
+        <SvgExpandArrow width={7} height={7} style={{ marginLeft: 5 }} />
+        {menuOpen && (
+          <Tooltip
+            position="bottom-left"
+            style={{ padding: 0 }}
+            onClose={() => setMenuOpen(false)}
+          >
+            <Menu onMenuSelect={onMenuSelect} items={items} />
+          </Tooltip>
+        )}
+      </Button>
+    );
+  }
+}
diff --git a/packages/desktop-client/src/components/sidebar/SidebarProvider.tsx b/packages/desktop-client/src/components/sidebar/SidebarProvider.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..717f3393e53e1d881b5a7e9c3b91e36f0c4e2375
--- /dev/null
+++ b/packages/desktop-client/src/components/sidebar/SidebarProvider.tsx
@@ -0,0 +1,52 @@
+// @ts-strict-ignore
+import React, {
+  createContext,
+  useState,
+  useContext,
+  useMemo,
+  type ReactNode,
+  type Dispatch,
+  type SetStateAction,
+} from 'react';
+
+import { useGlobalPref } from '../../hooks/useGlobalPref';
+import { useResponsive } from '../../ResponsiveProvider';
+
+type SidebarContextValue = {
+  hidden: boolean;
+  setHidden: Dispatch<SetStateAction<boolean>>;
+  floating: boolean;
+  alwaysFloats: boolean;
+};
+
+const SidebarContext = createContext<SidebarContextValue>(null);
+
+type SidebarProviderProps = {
+  children: ReactNode;
+};
+
+export function SidebarProvider({ children }: SidebarProviderProps) {
+  const [floatingSidebar] = useGlobalPref('floatingSidebar');
+  const [hidden, setHidden] = useState(true);
+  const { width } = useResponsive();
+  const alwaysFloats = width < 668;
+  const floating = floatingSidebar || alwaysFloats;
+
+  return (
+    <SidebarContext.Provider
+      value={{ hidden, setHidden, floating, alwaysFloats }}
+    >
+      {children}
+    </SidebarContext.Provider>
+  );
+}
+
+export function useSidebar() {
+  const { hidden, setHidden, floating, alwaysFloats } =
+    useContext(SidebarContext);
+
+  return useMemo(
+    () => ({ hidden, setHidden, floating, alwaysFloats }),
+    [hidden, setHidden, floating, alwaysFloats],
+  );
+}
diff --git a/packages/desktop-client/src/components/sidebar/SidebarWithData.tsx b/packages/desktop-client/src/components/sidebar/SidebarWithData.tsx
deleted file mode 100644
index 202e84bfe9c2c51d91af10353e333ba955675026..0000000000000000000000000000000000000000
--- a/packages/desktop-client/src/components/sidebar/SidebarWithData.tsx
+++ /dev/null
@@ -1,181 +0,0 @@
-// @ts-strict-ignore
-import React, { useState, useEffect } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
-
-import { type State } from 'loot-core/client/state-types';
-import { type AccountState } from 'loot-core/client/state-types/account';
-import { type PrefsState } from 'loot-core/client/state-types/prefs';
-import { type QueriesState } from 'loot-core/client/state-types/queries';
-import { closeBudget } from 'loot-core/src/client/actions/budgets';
-import * as Platform from 'loot-core/src/client/platform';
-import * as queries from 'loot-core/src/client/queries';
-import { send } from 'loot-core/src/platform/client/fetch';
-import { type LocalPrefs } from 'loot-core/src/types/prefs';
-
-import { useActions } from '../../hooks/useActions';
-import { useNavigate } from '../../hooks/useNavigate';
-import { SvgExpandArrow } from '../../icons/v0';
-import { styles, theme } from '../../style';
-import { Button } from '../common/Button';
-import { InitialFocus } from '../common/InitialFocus';
-import { Input } from '../common/Input';
-import { Menu } from '../common/Menu';
-import { Text } from '../common/Text';
-import { Tooltip } from '../tooltips';
-
-import { Sidebar } from './Sidebar';
-
-type EditableBudgetNameProps = {
-  prefs: LocalPrefs;
-  savePrefs: (prefs: Partial<LocalPrefs>) => Promise<void>;
-};
-
-function EditableBudgetName({ prefs, savePrefs }: EditableBudgetNameProps) {
-  const dispatch = useDispatch();
-  const navigate = useNavigate();
-  const [editing, setEditing] = useState(false);
-  const [menuOpen, setMenuOpen] = useState(false);
-
-  function onMenuSelect(type) {
-    setMenuOpen(false);
-
-    switch (type) {
-      case 'rename':
-        setEditing(true);
-        break;
-      case 'settings':
-        navigate('/settings');
-        break;
-      case 'help':
-        window.open('https://actualbudget.org/docs/', '_blank');
-        break;
-      case 'close':
-        dispatch(closeBudget());
-        break;
-      default:
-    }
-  }
-
-  const items = [
-    { name: 'rename', text: 'Rename budget' },
-    { name: 'settings', text: 'Settings' },
-    ...(Platform.isBrowser ? [{ name: 'help', text: 'Help' }] : []),
-    { name: 'close', text: 'Close file' },
-  ];
-
-  const onSaveChanges = async e => {
-    const inputEl = e.target;
-    const newBudgetName = inputEl.value;
-    if (newBudgetName.trim() !== '') {
-      await savePrefs({
-        budgetName: inputEl.value,
-      });
-      setEditing(false);
-    }
-  };
-
-  if (editing) {
-    return (
-      <InitialFocus>
-        <Input
-          style={{
-            width: 160,
-            fontSize: 16,
-            fontWeight: 500,
-          }}
-          defaultValue={prefs.budgetName}
-          onEnter={onSaveChanges}
-          onBlur={onSaveChanges}
-          onEscape={() => setEditing(false)}
-        />
-      </InitialFocus>
-    );
-  } else {
-    return (
-      <Button
-        type="bare"
-        color={theme.buttonNormalBorder}
-        style={{
-          fontSize: 16,
-          fontWeight: 500,
-          marginLeft: -5,
-          flex: '0 auto',
-        }}
-        onClick={() => setMenuOpen(true)}
-      >
-        <Text style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
-          {prefs.budgetName || 'A budget has no name'}
-        </Text>
-        <SvgExpandArrow width={7} height={7} style={{ marginLeft: 5 }} />
-        {menuOpen && (
-          <Tooltip
-            position="bottom-left"
-            style={{ padding: 0 }}
-            onClose={() => setMenuOpen(false)}
-          >
-            <Menu onMenuSelect={onMenuSelect} items={items} />
-          </Tooltip>
-        )}
-      </Button>
-    );
-  }
-}
-
-export function SidebarWithData() {
-  const accounts = useSelector<State, QueriesState['accounts']>(
-    state => state.queries.accounts,
-  );
-  const failedAccounts = useSelector<State, AccountState['failedAccounts']>(
-    state => state.account.failedAccounts,
-  );
-  const updatedAccounts = useSelector<State, QueriesState['updatedAccounts']>(
-    state => state.queries.updatedAccounts,
-  );
-  const prefs = useSelector<State, LocalPrefs>(state => state.prefs.local);
-  const floatingSidebar = useSelector<
-    State,
-    PrefsState['global']['floatingSidebar']
-  >(state => state.prefs.global.floatingSidebar);
-
-  const { getAccounts, replaceModal, savePrefs, saveGlobalPrefs } =
-    useActions();
-
-  useEffect(() => void getAccounts(), [getAccounts]);
-
-  async function onReorder(id, dropPos, targetId) {
-    if (dropPos === 'bottom') {
-      const idx = accounts.findIndex(a => a.id === targetId) + 1;
-      targetId = idx < accounts.length ? accounts[idx].id : null;
-    }
-
-    await send('account-move', { id, targetId });
-    await getAccounts();
-  }
-
-  return (
-    <Sidebar
-      budgetName={<EditableBudgetName prefs={prefs} savePrefs={savePrefs} />}
-      isFloating={floatingSidebar}
-      accounts={accounts}
-      failedAccounts={failedAccounts}
-      updatedAccounts={updatedAccounts}
-      getBalanceQuery={queries.accountBalance}
-      getAllAccountBalance={queries.allAccountBalance}
-      getOnBudgetBalance={queries.budgetedAccountBalance}
-      getOffBudgetBalance={queries.offbudgetAccountBalance}
-      onFloat={() => saveGlobalPrefs({ floatingSidebar: !floatingSidebar })}
-      onReorder={onReorder}
-      onAddAccount={() => replaceModal('add-account')}
-      showClosedAccounts={prefs['ui.showClosedAccounts']}
-      onToggleClosedAccounts={() =>
-        savePrefs({
-          'ui.showClosedAccounts': !prefs['ui.showClosedAccounts'],
-        })
-      }
-      style={{
-        flex: 1,
-        ...styles.darkScrollbar,
-      }}
-    />
-  );
-}
diff --git a/packages/desktop-client/src/components/sidebar/index.tsx b/packages/desktop-client/src/components/sidebar/index.tsx
index c3f02e448cb791efc14aad0e91d65ebe5be5c570..59f7b1628ad86e58eb71e1e735b8944694107e7e 100644
--- a/packages/desktop-client/src/components/sidebar/index.tsx
+++ b/packages/desktop-client/src/components/sidebar/index.tsx
@@ -1,71 +1,14 @@
-// @ts-strict-ignore
-import React, {
-  createContext,
-  useState,
-  useContext,
-  useMemo,
-  type ReactNode,
-  type Dispatch,
-  type SetStateAction,
-} from 'react';
-import { useSelector } from 'react-redux';
-
-import { type State } from 'loot-core/client/state-types';
-import { type PrefsState } from 'loot-core/client/state-types/prefs';
+import React from 'react';
 
+import { useGlobalPref } from '../../hooks/useGlobalPref';
 import { useResponsive } from '../../ResponsiveProvider';
 import { View } from '../common/View';
 
-import { SIDEBAR_WIDTH } from './Sidebar';
-import { SidebarWithData } from './SidebarWithData';
-
-type SidebarContextValue = {
-  hidden: boolean;
-  setHidden: Dispatch<SetStateAction<boolean>>;
-  floating: boolean;
-  alwaysFloats: boolean;
-};
-
-const SidebarContext = createContext<SidebarContextValue>(null);
-
-type SidebarProviderProps = {
-  children: ReactNode;
-};
-
-export function SidebarProvider({ children }: SidebarProviderProps) {
-  const floatingSidebar = useSelector<
-    State,
-    PrefsState['global']['floatingSidebar']
-  >(state => state.prefs.global.floatingSidebar);
-  const [hidden, setHidden] = useState(true);
-  const { width } = useResponsive();
-  const alwaysFloats = width < 668;
-  const floating = floatingSidebar || alwaysFloats;
-
-  return (
-    <SidebarContext.Provider
-      value={{ hidden, setHidden, floating, alwaysFloats }}
-    >
-      {children}
-    </SidebarContext.Provider>
-  );
-}
-
-export function useSidebar() {
-  const { hidden, setHidden, floating, alwaysFloats } =
-    useContext(SidebarContext);
-
-  return useMemo(
-    () => ({ hidden, setHidden, floating, alwaysFloats }),
-    [hidden, setHidden, floating, alwaysFloats],
-  );
-}
+import { SIDEBAR_WIDTH, Sidebar } from './Sidebar';
+import { useSidebar } from './SidebarProvider';
 
 export function FloatableSidebar() {
-  const floatingSidebar = useSelector<
-    State,
-    PrefsState['global']['floatingSidebar']
-  >(state => state.prefs.global.floatingSidebar);
+  const [floatingSidebar] = useGlobalPref('floatingSidebar');
 
   const sidebar = useSidebar();
   const { isNarrowWidth } = useResponsive();
@@ -80,11 +23,13 @@ export function FloatableSidebar() {
               e.stopPropagation();
               sidebar.setHidden(false);
             }
-          : null
+          : undefined
+      }
+      onMouseLeave={
+        sidebarShouldFloat ? () => sidebar.setHidden(true) : undefined
       }
-      onMouseLeave={sidebarShouldFloat ? () => sidebar.setHidden(true) : null}
       style={{
-        position: sidebarShouldFloat ? 'absolute' : null,
+        position: sidebarShouldFloat ? 'absolute' : undefined,
         top: 12,
         // If not floating, the -50 takes into account the transform below
         bottom: sidebarShouldFloat ? 12 : -50,
@@ -105,7 +50,7 @@ export function FloatableSidebar() {
           'transform .5s, box-shadow .5s, border-radius .5s, bottom .5s',
       }}
     >
-      <SidebarWithData />
+      <Sidebar />
     </View>
   );
 }
diff --git a/packages/desktop-client/src/components/transactions/MobileTransaction.jsx b/packages/desktop-client/src/components/transactions/MobileTransaction.jsx
index bda688058a8c25341f8b8ced7f5ae0513d5f8623..bbce43aee6d4860500ea6d5700c3bec45b70c224 100644
--- a/packages/desktop-client/src/components/transactions/MobileTransaction.jsx
+++ b/packages/desktop-client/src/components/transactions/MobileTransaction.jsx
@@ -46,9 +46,12 @@ import {
   groupById,
 } from 'loot-core/src/shared/util';
 
+import { useAccounts } from '../../hooks/useAccounts';
 import { useActions } from '../../hooks/useActions';
 import { useCategories } from '../../hooks/useCategories';
+import { useDateFormat } from '../../hooks/useDateFormat';
 import { useNavigate } from '../../hooks/useNavigate';
+import { usePayees } from '../../hooks/usePayees';
 import { useSetThemeColor } from '../../hooks/useSetThemeColor';
 import {
   SingleActiveEditFormProvider,
@@ -939,11 +942,6 @@ function TransactionEditUnconnected(props) {
   useSetThemeColor(theme.mobileViewTheme);
 
   useEffect(() => {
-    // May as well update categories / accounts when transaction ID changes
-    props.getCategories();
-    props.getAccounts();
-    props.getPayees();
-
     async function fetchTransaction() {
       // Query for the transaction based on the ID with grouped splits.
       //
@@ -1110,12 +1108,10 @@ function TransactionEditUnconnected(props) {
 
 export const TransactionEdit = props => {
   const { list: categories } = useCategories();
-  const payees = useSelector(state => state.queries.payees);
+  const payees = usePayees();
   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 accounts = useAccounts();
+  const dateFormat = useDateFormat() || 'MM/dd/yyyy';
   const actions = useActions();
 
   return (
diff --git a/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx b/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx
index ea43f6adbd69b86bb499e8a397b01e24b37d71b6..736df89d280b3a25e3eb7bf379be9c9d8c8155e6 100644
--- a/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx
+++ b/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx
@@ -1,5 +1,4 @@
-import React, { memo, useCallback, useMemo } from 'react';
-import { useSelector } from 'react-redux';
+import React, { memo, useMemo, useCallback } from 'react';
 
 import {
   format as formatDate,
@@ -13,8 +12,11 @@ import {
 } from 'loot-core/src/client/reducers/queries';
 import { integerToCurrency } from 'loot-core/src/shared/util';
 
+import { useAccounts } from '../../hooks/useAccounts';
 import { useCategories } from '../../hooks/useCategories';
-import { useSelectedDispatch, useSelectedItems } from '../../hooks/useSelected';
+import { useDateFormat } from '../../hooks/useDateFormat';
+import { usePayees } from '../../hooks/usePayees';
+import { useSelectedItems, useSelectedDispatch } from '../../hooks/useSelected';
 import { SvgArrowsSynchronize } from '../../icons/v2';
 import { styles, theme } from '../../style';
 import { Cell, Field, Row, SelectCell, Table } from '../table';
@@ -141,13 +143,9 @@ export function SimpleTransactionsTable({
   style,
 }) {
   const { grouped: categories } = useCategories();
-  const { payees, accounts, dateFormat } = useSelector(state => {
-    return {
-      payees: state.queries.payees,
-      accounts: state.queries.accounts,
-      dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy',
-    };
-  });
+  const payees = usePayees();
+  const accounts = useAccounts();
+  const dateFormat = useDateFormat() || 'MM/dd/yyyy';
   const selectedItems = useSelectedItems();
   const dispatchSelected = useSelectedDispatch();
   const memoFields = useMemo(() => fields, [JSON.stringify(fields)]);
diff --git a/packages/desktop-client/src/components/util/DisplayId.tsx b/packages/desktop-client/src/components/util/DisplayId.tsx
index db3982cbeac3b9ad67ec25c2014f2519c8b1f7af..34c5f593accb1e7611bd59c5566fc16658227886 100644
--- a/packages/desktop-client/src/components/util/DisplayId.tsx
+++ b/packages/desktop-client/src/components/util/DisplayId.tsx
@@ -1,9 +1,8 @@
 // @ts-strict-ignore
 import React from 'react';
 
-import { CachedAccounts } from 'loot-core/src/client/data-hooks/accounts';
-import { CachedPayees } from 'loot-core/src/client/data-hooks/payees';
-
+import { useAccount } from '../../hooks/useAccount';
+import { usePayee } from '../../hooks/usePayee';
 import { theme } from '../../style';
 import { Text } from '../common/Text';
 
@@ -18,33 +17,33 @@ export function DisplayId({
   id,
   noneColor = theme.pageTextSubdued,
 }: DisplayIdProps) {
-  let DataComponent;
-
-  switch (type) {
-    case 'payees':
-      DataComponent = CachedPayees;
-      break;
-    case 'accounts':
-      DataComponent = CachedAccounts;
-      break;
-    default:
-      throw new Error('DisplayId: unknown object type: ' + type);
-  }
+  return type === 'accounts' ? (
+    <AccountDisplayId id={id} noneColor={noneColor} />
+  ) : (
+    <PayeeDisplayId id={id} noneColor={noneColor} />
+  );
+}
 
+function AccountDisplayId({ id, noneColor }) {
+  const account = useAccount(id);
   return (
-    <DataComponent idKey={true}>
-      {data => {
-        const item = data[id];
+    <Text
+      style={account == null ? { color: noneColor } : null}
+      title={account ? account.name : 'None'}
+    >
+      {account ? account.name : 'None'}
+    </Text>
+  );
+}
 
-        return (
-          <Text
-            style={item == null ? { color: noneColor } : null}
-            title={item ? item.name : 'None'}
-          >
-            {item ? item.name : 'None'}
-          </Text>
-        );
-      }}
-    </DataComponent>
+function PayeeDisplayId({ id, noneColor }) {
+  const payee = usePayee(id);
+  return (
+    <Text
+      style={payee == null ? { color: noneColor } : null}
+      title={payee ? payee.name : 'None'}
+    >
+      {payee ? payee.name : 'None'}
+    </Text>
   );
 }
diff --git a/packages/desktop-client/src/components/util/GenericInput.jsx b/packages/desktop-client/src/components/util/GenericInput.jsx
index b42826e01baa868339b82e2f24f0376a21ce4d44..bf2e3c53d302eef47b1775b122c3854d01915a01 100644
--- a/packages/desktop-client/src/components/util/GenericInput.jsx
+++ b/packages/desktop-client/src/components/util/GenericInput.jsx
@@ -4,6 +4,7 @@ import { useSelector } from 'react-redux';
 import { getMonthYearFormat } from 'loot-core/src/shared/months';
 
 import { useCategories } from '../../hooks/useCategories';
+import { useDateFormat } from '../../hooks/useDateFormat';
 import { AccountAutocomplete } from '../autocomplete/AccountAutocomplete';
 import { Autocomplete } from '../autocomplete/Autocomplete';
 import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete';
@@ -27,9 +28,7 @@ export function GenericInput({
 }) {
   const { grouped: categoryGroups } = useCategories();
   const saved = useSelector(state => state.queries.saved);
-  const dateFormat = useSelector(
-    state => state.prefs.local.dateFormat || 'MM/dd/yyyy',
-  );
+  const dateFormat = useDateFormat() || 'MM/dd/yyyy';
 
   // This makes the UI more resilient in case of faulty data
   if (multi && !Array.isArray(value)) {
diff --git a/packages/desktop-client/src/hooks/useAccount.ts b/packages/desktop-client/src/hooks/useAccount.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e3da8f35e6b29ba3bce8f648e8b1d2c948e207ea
--- /dev/null
+++ b/packages/desktop-client/src/hooks/useAccount.ts
@@ -0,0 +1,8 @@
+import { useMemo } from 'react';
+
+import { useAccounts } from './useAccounts';
+
+export function useAccount(id: string) {
+  const accounts = useAccounts();
+  return useMemo(() => accounts.find(a => a.id === id), [id, accounts]);
+}
diff --git a/packages/desktop-client/src/hooks/useAccounts.ts b/packages/desktop-client/src/hooks/useAccounts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4c44e9cec937a851e365a1a36e3fb44abfa6a3b9
--- /dev/null
+++ b/packages/desktop-client/src/hooks/useAccounts.ts
@@ -0,0 +1,20 @@
+import { useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { getAccounts } from 'loot-core/src/client/actions';
+import { type State } from 'loot-core/src/client/state-types';
+
+export function useAccounts() {
+  const dispatch = useDispatch();
+  const accountsLoaded = useSelector(
+    (state: State) => state.queries.accountsLoaded,
+  );
+
+  useEffect(() => {
+    if (!accountsLoaded) {
+      dispatch(getAccounts());
+    }
+  }, []);
+
+  return useSelector(state => state.queries.accounts);
+}
diff --git a/packages/desktop-client/src/hooks/useBudgetedAccounts.ts b/packages/desktop-client/src/hooks/useBudgetedAccounts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dbd8e1f53db9fd64ec3c6725c3093a85ad455ffd
--- /dev/null
+++ b/packages/desktop-client/src/hooks/useBudgetedAccounts.ts
@@ -0,0 +1,14 @@
+import { useMemo } from 'react';
+
+import { useAccounts } from './useAccounts';
+
+export function useBudgetedAccounts() {
+  const accounts = useAccounts();
+  return useMemo(
+    () =>
+      accounts.filter(
+        account => account.closed === 0 && account.offbudget === 0,
+      ),
+    [accounts],
+  );
+}
diff --git a/packages/desktop-client/src/hooks/useCategories.ts b/packages/desktop-client/src/hooks/useCategories.ts
index 8e2730915419afd2caf0e5986a789c89fac64c2c..4c85fdfaff2f298030e2ca8f55f01967d596a72c 100644
--- a/packages/desktop-client/src/hooks/useCategories.ts
+++ b/packages/desktop-client/src/hooks/useCategories.ts
@@ -1,25 +1,20 @@
 import { useEffect } from 'react';
-import { useSelector } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
 
-import { type State } from 'loot-core/client/state-types';
-import { type QueriesState } from 'loot-core/client/state-types/queries';
-
-import { useActions } from './useActions';
+import { getCategories } from 'loot-core/src/client/actions';
+import { type State } from 'loot-core/src/client/state-types';
 
 export function useCategories() {
-  const { getCategories } = useActions();
-
-  const categories = useSelector<State, QueriesState['categories']['list']>(
-    state => state.queries.categories.list,
+  const dispatch = useDispatch();
+  const categoriesLoaded = useSelector(
+    (state: State) => state.queries.categoriesLoaded,
   );
 
   useEffect(() => {
-    if (categories.length === 0) {
-      getCategories();
+    if (!categoriesLoaded) {
+      dispatch(getCategories());
     }
   }, []);
 
-  return useSelector<State, QueriesState['categories']>(
-    state => state.queries.categories,
-  );
+  return useSelector(state => state.queries.categories);
 }
diff --git a/packages/desktop-client/src/hooks/useClosedAccounts.ts b/packages/desktop-client/src/hooks/useClosedAccounts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..85aa5b92040174b9232f8ab14c3ce300a323ab30
--- /dev/null
+++ b/packages/desktop-client/src/hooks/useClosedAccounts.ts
@@ -0,0 +1,11 @@
+import { useMemo } from 'react';
+
+import { useAccounts } from './useAccounts';
+
+export function useClosedAccounts() {
+  const accounts = useAccounts();
+  return useMemo(
+    () => accounts.filter(account => account.closed === 1),
+    [accounts],
+  );
+}
diff --git a/packages/desktop-client/src/hooks/useDateFormat.ts b/packages/desktop-client/src/hooks/useDateFormat.ts
new file mode 100644
index 0000000000000000000000000000000000000000..258f156e1fc978566829a11da3f0de6ece4d549b
--- /dev/null
+++ b/packages/desktop-client/src/hooks/useDateFormat.ts
@@ -0,0 +1,7 @@
+import { useSelector } from 'react-redux';
+
+import { type State } from 'loot-core/src/client/state-types';
+
+export function useDateFormat() {
+  return useSelector((state: State) => state.prefs.local?.dateFormat);
+}
diff --git a/packages/desktop-client/src/hooks/useFailedAccounts.ts b/packages/desktop-client/src/hooks/useFailedAccounts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..86aeb89959d0f9f047b951dc94c185b41419d91e
--- /dev/null
+++ b/packages/desktop-client/src/hooks/useFailedAccounts.ts
@@ -0,0 +1,7 @@
+import { useSelector } from 'react-redux';
+
+import { type State } from 'loot-core/src/client/state-types';
+
+export function useFailedAccounts() {
+  return useSelector((state: State) => state.account.failedAccounts);
+}
diff --git a/packages/desktop-client/src/hooks/useFeatureFlag.ts b/packages/desktop-client/src/hooks/useFeatureFlag.ts
index 40b79b84dba20f16708533742ecbb3caf71f642d..5550cfed2cd0f17084c21e036abdd244f903ef8c 100644
--- a/packages/desktop-client/src/hooks/useFeatureFlag.ts
+++ b/packages/desktop-client/src/hooks/useFeatureFlag.ts
@@ -1,8 +1,7 @@
 // @ts-strict-ignore
 import { useSelector } from 'react-redux';
 
-import { type State } from 'loot-core/client/state-types';
-import { type PrefsState } from 'loot-core/client/state-types/prefs';
+import { type State } from 'loot-core/src/client/state-types';
 import type { FeatureFlag } from 'loot-core/src/types/prefs';
 
 const DEFAULT_FEATURE_FLAG_STATE: Record<FeatureFlag, boolean> = {
@@ -15,13 +14,11 @@ const DEFAULT_FEATURE_FLAG_STATE: Record<FeatureFlag, boolean> = {
 };
 
 export function useFeatureFlag(name: FeatureFlag): boolean {
-  return useSelector<State, PrefsState['local'][`flags.${FeatureFlag}`]>(
-    state => {
-      const value = state.prefs.local[`flags.${name}`];
+  return useSelector((state: State) => {
+    const value = state.prefs.local[`flags.${name}`];
 
-      return value === undefined
-        ? DEFAULT_FEATURE_FLAG_STATE[name] || false
-        : value;
-    },
-  );
+    return value === undefined
+      ? DEFAULT_FEATURE_FLAG_STATE[name] || false
+      : value;
+  });
 }
diff --git a/packages/desktop-client/src/hooks/useGlobalPref.ts b/packages/desktop-client/src/hooks/useGlobalPref.ts
new file mode 100644
index 0000000000000000000000000000000000000000..02d5773ac62d0a4c91e9cbf208cc4053a1cb9341
--- /dev/null
+++ b/packages/desktop-client/src/hooks/useGlobalPref.ts
@@ -0,0 +1,27 @@
+import { useCallback } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { saveGlobalPrefs } from 'loot-core/src/client/actions';
+import { type State } from 'loot-core/src/client/state-types';
+import { type GlobalPrefs } from 'loot-core/src/types/prefs';
+
+type SetGlobalPrefAction<K extends keyof GlobalPrefs> = (
+  value: GlobalPrefs[K],
+) => void;
+
+export function useGlobalPref<K extends keyof GlobalPrefs>(
+  prefName: K,
+): [GlobalPrefs[K], SetGlobalPrefAction<K>] {
+  const dispatch = useDispatch();
+  const setGlobalPref = useCallback<SetGlobalPrefAction<K>>(
+    value => {
+      dispatch(saveGlobalPrefs({ [prefName]: value } as GlobalPrefs));
+    },
+    [prefName, dispatch],
+  );
+  const globalPref = useSelector(
+    (state: State) => state.prefs.global?.[prefName] as GlobalPrefs[K],
+  );
+
+  return [globalPref, setGlobalPref];
+}
diff --git a/packages/desktop-client/src/hooks/useLocalPref.ts b/packages/desktop-client/src/hooks/useLocalPref.ts
new file mode 100644
index 0000000000000000000000000000000000000000..70a50cb554568cf14ea5bc1bb7c48d3178bdcb5f
--- /dev/null
+++ b/packages/desktop-client/src/hooks/useLocalPref.ts
@@ -0,0 +1,27 @@
+import { useCallback } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { savePrefs } from 'loot-core/src/client/actions';
+import { type State } from 'loot-core/src/client/state-types';
+import { type LocalPrefs } from 'loot-core/src/types/prefs';
+
+type SetLocalPrefAction<K extends keyof LocalPrefs> = (
+  value: LocalPrefs[K],
+) => void;
+
+export function useLocalPref<K extends keyof LocalPrefs>(
+  prefName: K,
+): [LocalPrefs[K], SetLocalPrefAction<K>] {
+  const dispatch = useDispatch();
+  const setLocalPref = useCallback<SetLocalPrefAction<K>>(
+    value => {
+      dispatch(savePrefs({ [prefName]: value } as LocalPrefs));
+    },
+    [prefName, dispatch],
+  );
+  const localPref = useSelector(
+    (state: State) => state.prefs.local?.[prefName] as LocalPrefs[K],
+  );
+
+  return [localPref, setLocalPref];
+}
diff --git a/packages/desktop-client/src/hooks/useLocalPrefs.ts b/packages/desktop-client/src/hooks/useLocalPrefs.ts
new file mode 100644
index 0000000000000000000000000000000000000000..870bef808e340661745e538aa3b619e91c60e34d
--- /dev/null
+++ b/packages/desktop-client/src/hooks/useLocalPrefs.ts
@@ -0,0 +1,7 @@
+import { useSelector } from 'react-redux';
+
+import { type State } from 'loot-core/src/client/state-types';
+
+export function useLocalPrefs() {
+  return useSelector((state: State) => state.prefs.local);
+}
diff --git a/packages/desktop-client/src/hooks/useOffBudgetAccounts.ts b/packages/desktop-client/src/hooks/useOffBudgetAccounts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..71a5db919bfa3608880bdae6714826f7c4a83df8
--- /dev/null
+++ b/packages/desktop-client/src/hooks/useOffBudgetAccounts.ts
@@ -0,0 +1,14 @@
+import { useMemo } from 'react';
+
+import { useAccounts } from './useAccounts';
+
+export function useOffBudgetAccounts() {
+  const accounts = useAccounts();
+  return useMemo(
+    () =>
+      accounts.filter(
+        account => account.closed === 0 && account.offbudget === 1,
+      ),
+    [accounts],
+  );
+}
diff --git a/packages/desktop-client/src/hooks/usePayee.ts b/packages/desktop-client/src/hooks/usePayee.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2606c60a875f4d5d18429c4e4246f0ed0ef12afb
--- /dev/null
+++ b/packages/desktop-client/src/hooks/usePayee.ts
@@ -0,0 +1,8 @@
+import { useMemo } from 'react';
+
+import { usePayees } from './usePayees';
+
+export function usePayee(id: string) {
+  const payees = usePayees();
+  return useMemo(() => payees.find(p => p.id === id), [id, payees]);
+}
diff --git a/packages/desktop-client/src/hooks/usePayees.ts b/packages/desktop-client/src/hooks/usePayees.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cf51d6b9a7f1baf5977bdae3943596c5bbca3564
--- /dev/null
+++ b/packages/desktop-client/src/hooks/usePayees.ts
@@ -0,0 +1,20 @@
+import { useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { getPayees } from 'loot-core/src/client/actions';
+import { type State } from 'loot-core/src/client/state-types';
+
+export function usePayees() {
+  const dispatch = useDispatch();
+  const payeesLoaded = useSelector(
+    (state: State) => state.queries.payeesLoaded,
+  );
+
+  useEffect(() => {
+    if (!payeesLoaded) {
+      dispatch(getPayees());
+    }
+  }, []);
+
+  return useSelector(state => state.queries.payees);
+}
diff --git a/packages/desktop-client/src/hooks/usePrivacyMode.ts b/packages/desktop-client/src/hooks/usePrivacyMode.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ffa633bc04f3c7a2c6e58ab426aa9c011b1d1f10
--- /dev/null
+++ b/packages/desktop-client/src/hooks/usePrivacyMode.ts
@@ -0,0 +1,9 @@
+import { useSelector } from 'react-redux';
+
+import { type State } from 'loot-core/src/client/state-types';
+
+export function usePrivacyMode() {
+  return useSelector(
+    (state: State) => state.prefs.local?.isPrivacyEnabled ?? false,
+  );
+}
diff --git a/packages/desktop-client/src/hooks/useSelected.tsx b/packages/desktop-client/src/hooks/useSelected.tsx
index 398350e0dd1d1c63eb10e5551f76815bd94a7bca..63a0437a815169a89dce1a65073923438a560869 100644
--- a/packages/desktop-client/src/hooks/useSelected.tsx
+++ b/packages/desktop-client/src/hooks/useSelected.tsx
@@ -12,8 +12,7 @@ import React, {
 } from 'react';
 import { useSelector } from 'react-redux';
 
-import { type State } from 'loot-core/client/state-types';
-import { type AppState } from 'loot-core/client/state-types/app';
+import { type State } from 'loot-core/src/client/state-types';
 import { listen } from 'loot-core/src/platform/client/fetch';
 import * as undo from 'loot-core/src/platform/client/undo';
 import { type UndoState } from 'loot-core/src/server/undo';
@@ -210,9 +209,7 @@ export function useSelected<T extends Item>(
     return () => undo.setUndoState('selectedItems', prevState);
   }, [state.selectedItems]);
 
-  const lastUndoState = useSelector<State, AppState['lastUndoState']>(
-    state => state.app.lastUndoState,
-  );
+  const lastUndoState = useSelector((state: State) => state.app.lastUndoState);
 
   useEffect(() => {
     function onUndo({ messages, undoTag }: UndoState) {
diff --git a/packages/desktop-client/src/hooks/useSyncServerStatus.ts b/packages/desktop-client/src/hooks/useSyncServerStatus.ts
index 2b48a0c6f5779c3cfb43020229f1063613784851..d788340bee9f2a706c0510bdf8f2f350d6af10c6 100644
--- a/packages/desktop-client/src/hooks/useSyncServerStatus.ts
+++ b/packages/desktop-client/src/hooks/useSyncServerStatus.ts
@@ -1,7 +1,6 @@
 import { useSelector } from 'react-redux';
 
-import { type State } from 'loot-core/client/state-types';
-import { type UserState } from 'loot-core/client/state-types/user';
+import { type State } from 'loot-core/src/client/state-types';
 
 import { useServerURL } from '../components/ServerContext';
 
@@ -9,9 +8,7 @@ export type SyncServerStatus = 'offline' | 'no-server' | 'online';
 
 export function useSyncServerStatus(): SyncServerStatus {
   const serverUrl = useServerURL();
-  const userData = useSelector<State, UserState['data']>(
-    state => state.user.data,
-  );
+  const userData = useSelector((state: State) => state.user.data);
 
   if (!serverUrl) {
     return 'no-server';
diff --git a/packages/desktop-client/src/hooks/useUpdatedAccounts.ts b/packages/desktop-client/src/hooks/useUpdatedAccounts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..483d22c9b5d69ea02e003ff316f25f4a66efffc3
--- /dev/null
+++ b/packages/desktop-client/src/hooks/useUpdatedAccounts.ts
@@ -0,0 +1,7 @@
+import { useSelector } from 'react-redux';
+
+import { type State } from 'loot-core/src/client/state-types';
+
+export function useUpdatedAccounts() {
+  return useSelector((state: State) => state.queries.updatedAccounts);
+}
diff --git a/packages/desktop-client/src/style/theme.tsx b/packages/desktop-client/src/style/theme.tsx
index 7cd3b5751fd9903546a18f6c451a057d153fb72d..fe6b2374d14d31df10a7aded560789d609fa8ef0 100644
--- a/packages/desktop-client/src/style/theme.tsx
+++ b/packages/desktop-client/src/style/theme.tsx
@@ -1,12 +1,11 @@
 // @ts-strict-ignore
 import { useEffect, useState } from 'react';
-import { useSelector } from 'react-redux';
 
-import { type State } from 'loot-core/client/state-types';
-import { type PrefsState } from 'loot-core/client/state-types/prefs';
 import { isNonProductionEnvironment } from 'loot-core/src/shared/environment';
 import type { Theme } from 'loot-core/src/types/prefs';
 
+import { useGlobalPref } from '../hooks/useGlobalPref';
+
 import * as darkTheme from './themes/dark';
 import * as developmentTheme from './themes/development';
 import * as lightTheme from './themes/light';
@@ -24,16 +23,13 @@ export const themeOptions = Object.entries(themes).map(
   ([key, { name }]) => [key, name] as [Theme, string],
 );
 
-export function useTheme(): Theme {
-  return (
-    useSelector<State, PrefsState['global']['theme']>(
-      state => state.prefs.global?.theme,
-    ) || 'light'
-  );
+export function useTheme() {
+  const [theme = 'light', setThemePref] = useGlobalPref('theme');
+  return [theme, setThemePref] as const;
 }
 
 export function ThemeStyle() {
-  const theme = useTheme();
+  const [theme] = useTheme();
   const [themeColors, setThemeColors] = useState<
     typeof lightTheme | typeof darkTheme | undefined
   >(undefined);
diff --git a/packages/loot-core/src/client/actions/account.ts b/packages/loot-core/src/client/actions/account.ts
index c8349d84c578607b951c5dedb941ca14274a4bf7..e7f7f269b4b2d67fab7208cf71808e63d2b3a215 100644
--- a/packages/loot-core/src/client/actions/account.ts
+++ b/packages/loot-core/src/client/actions/account.ts
@@ -263,3 +263,10 @@ export function markAccountRead(accountId): MarkAccountReadAction {
     accountId,
   };
 }
+
+export function moveAccount(id, targetId) {
+  return async (dispatch: Dispatch) => {
+    await send('account-move', { id, targetId });
+    dispatch(getAccounts());
+  };
+}
diff --git a/packages/loot-core/src/client/actions/prefs.ts b/packages/loot-core/src/client/actions/prefs.ts
index b93a38eb3894e86f08c32d1bc2139c0f35d5505e..5dbca103f50908b1787b2da89deb92ac5330e2c2 100644
--- a/packages/loot-core/src/client/actions/prefs.ts
+++ b/packages/loot-core/src/client/actions/prefs.ts
@@ -26,7 +26,7 @@ export function loadPrefs() {
   };
 }
 
-export function savePrefs(prefs: Partial<prefs.LocalPrefs>) {
+export function savePrefs(prefs: prefs.LocalPrefs) {
   return async (dispatch: Dispatch) => {
     await send('save-prefs', prefs);
     dispatch({
@@ -48,7 +48,7 @@ export function loadGlobalPrefs() {
   };
 }
 
-export function saveGlobalPrefs(prefs: Partial<prefs.GlobalPrefs>) {
+export function saveGlobalPrefs(prefs: prefs.GlobalPrefs) {
   return async (dispatch: Dispatch) => {
     await send('save-global-prefs', prefs);
     dispatch({
diff --git a/packages/loot-core/src/client/data-hooks/accounts.tsx b/packages/loot-core/src/client/data-hooks/accounts.tsx
deleted file mode 100644
index c2460477fa68879174e35e1b767ae9df8a6fbbd3..0000000000000000000000000000000000000000
--- a/packages/loot-core/src/client/data-hooks/accounts.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-// @ts-strict-ignore
-import React, { createContext, useContext } from 'react';
-
-import { q } from '../../shared/query';
-import { type AccountEntity } from '../../types/models';
-import { useLiveQuery } from '../query-hooks';
-import { getAccountsById } from '../reducers/queries';
-
-function useAccounts(): AccountEntity[] {
-  return useLiveQuery(() => q('accounts').select('*'), []);
-}
-
-const AccountsContext = createContext<AccountEntity[]>(null);
-
-export function AccountsProvider({ children }) {
-  const data = useAccounts();
-  return (
-    <AccountsContext.Provider value={data}>{children}</AccountsContext.Provider>
-  );
-}
-
-export function CachedAccounts({ children, idKey }) {
-  const data = useCachedAccounts({ idKey });
-  return children(data);
-}
-
-export function useCachedAccounts(): AccountEntity[];
-export function useCachedAccounts({
-  idKey,
-}: {
-  idKey: boolean;
-}): Record<AccountEntity['id'], AccountEntity>;
-export function useCachedAccounts({ idKey }: { idKey?: boolean } = {}) {
-  const data = useContext(AccountsContext);
-  return idKey && data ? getAccountsById(data) : data;
-}
diff --git a/packages/loot-core/src/client/data-hooks/payees.tsx b/packages/loot-core/src/client/data-hooks/payees.tsx
deleted file mode 100644
index 4299aa9a31f6fe354a05fe0fdfae9ad54082245e..0000000000000000000000000000000000000000
--- a/packages/loot-core/src/client/data-hooks/payees.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-// @ts-strict-ignore
-import React, { createContext, useContext } from 'react';
-
-import { q } from '../../shared/query';
-import { type PayeeEntity } from '../../types/models';
-import { useLiveQuery } from '../query-hooks';
-import { getPayeesById } from '../reducers/queries';
-
-function usePayees(): PayeeEntity[] {
-  return useLiveQuery(() => q('payees').select('*'), []);
-}
-
-const PayeesContext = createContext<PayeeEntity[]>(null);
-
-export function PayeesProvider({ children }) {
-  const data = usePayees();
-  return (
-    <PayeesContext.Provider value={data}>{children}</PayeesContext.Provider>
-  );
-}
-
-export function CachedPayees({ children, idKey }) {
-  const data = useCachedPayees({ idKey });
-  return children(data);
-}
-
-export function useCachedPayees(): PayeeEntity[];
-export function useCachedPayees({
-  idKey,
-}: {
-  idKey: boolean;
-}): Record<PayeeEntity['id'], PayeeEntity>;
-export function useCachedPayees({ idKey }: { idKey?: boolean } = {}) {
-  const data = useContext(PayeesContext);
-  return idKey && data ? getPayeesById(data) : data;
-}
diff --git a/packages/loot-core/src/client/privacy.ts b/packages/loot-core/src/client/privacy.ts
deleted file mode 100644
index 295bba5e510d77b821968441bd8f1717036572a8..0000000000000000000000000000000000000000
--- a/packages/loot-core/src/client/privacy.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { useSelector } from 'react-redux';
-
-export function usePrivacyMode() {
-  return useSelector(state => state.prefs?.local?.isPrivacyEnabled ?? false);
-}
diff --git a/packages/loot-core/src/client/reducers/queries.ts b/packages/loot-core/src/client/reducers/queries.ts
index 470c3591692ada59de9fb8cf2278306a57cbc990..b0c38817a240fc7a79992e91252d3a9dd194220f 100644
--- a/packages/loot-core/src/client/reducers/queries.ts
+++ b/packages/loot-core/src/client/reducers/queries.ts
@@ -13,11 +13,14 @@ const initialState: QueriesState = {
   lastTransaction: null,
   updatedAccounts: [],
   accounts: [],
+  accountsLoaded: false,
   categories: {
     grouped: [],
     list: [],
   },
+  categoriesLoaded: false,
   payees: [],
+  payeesLoaded: false,
   earliestTransaction: null,
 };
 
@@ -56,6 +59,7 @@ export function update(state = initialState, action: Action): QueriesState {
       return {
         ...state,
         accounts: action.accounts,
+        accountsLoaded: true,
       };
     case constants.UPDATE_ACCOUNT: {
       return {
@@ -72,11 +76,13 @@ export function update(state = initialState, action: Action): QueriesState {
       return {
         ...state,
         categories: action.categories,
+        categoriesLoaded: true,
       };
     case constants.LOAD_PAYEES:
       return {
         ...state,
         payees: action.payees,
+        payeesLoaded: true,
       };
     default:
   }
diff --git a/packages/loot-core/src/client/state-types/modals.d.ts b/packages/loot-core/src/client/state-types/modals.d.ts
index e25e38feb327a3ee438d2714547abcecc13d6115..72cd5dbc54b407d3099fb351de719e3b3d1043ac 100644
--- a/packages/loot-core/src/client/state-types/modals.d.ts
+++ b/packages/loot-core/src/client/state-types/modals.d.ts
@@ -42,7 +42,7 @@ type FinanceModals = {
     syncSource?: AccountSyncSource;
   };
 
-  'confirm-category-delete': { onDelete: () => void } & (
+  'confirm-category-delete': { onDelete: (categoryId: string) => void } & (
     | { category: string }
     | { group: string }
   );
diff --git a/packages/loot-core/src/client/state-types/queries.d.ts b/packages/loot-core/src/client/state-types/queries.d.ts
index a7f18526fe008822ed97a71d8d31c8cfe289000a..0d511868b39da8e2d1eeab25420dc2760fb7b26b 100644
--- a/packages/loot-core/src/client/state-types/queries.d.ts
+++ b/packages/loot-core/src/client/state-types/queries.d.ts
@@ -8,8 +8,11 @@ export type QueriesState = {
   lastTransaction: unknown | null;
   updatedAccounts: string[];
   accounts: AccountEntity[];
+  accountsLoaded: boolean;
   categories: Awaited<ReturnType<Handlers['get-categories']>>;
+  categoriesLoaded: boolean;
   payees: Awaited<ReturnType<Handlers['payees-get']>>;
+  payeesLoaded: boolean;
   earliestTransaction: unknown | null;
 };
 
diff --git a/packages/loot-core/src/shared/categories.ts b/packages/loot-core/src/shared/categories.ts
deleted file mode 100644
index 9e2c4c190aa28febf655db0486662f7402117412..0000000000000000000000000000000000000000
--- a/packages/loot-core/src/shared/categories.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-// @ts-strict-ignore
-export function addCategory(categoryGroups, cat) {
-  return categoryGroups.map(group => {
-    if (group.id === cat.cat_group) {
-      group.categories = [cat, ...group.categories];
-    }
-    return { ...group };
-  });
-}
-
-export function updateCategory(categoryGroups, category) {
-  return categoryGroups.map(group => {
-    if (group.id === category.cat_group) {
-      group.categories = group.categories.map(c => {
-        if (c.id === category.id) {
-          return { ...c, ...category };
-        }
-        return c;
-      });
-    }
-    return group;
-  });
-}
-
-export function moveCategory(categoryGroups, id, groupId, targetId) {
-  if (id === targetId) {
-    return categoryGroups;
-  }
-
-  let moveCat = categoryGroups.reduce((value, group) => {
-    return value || group.categories.find(cat => cat.id === id);
-  }, null);
-
-  // Update the group id on the category
-  moveCat = { ...moveCat, cat_group: groupId };
-
-  return categoryGroups.map(group => {
-    if (group.id === groupId) {
-      group.categories = group.categories.reduce((cats, cat) => {
-        if (cat.id === targetId) {
-          cats.push(moveCat);
-          cats.push(cat);
-        } else if (cat.id !== id) {
-          cats.push(cat);
-        }
-        return cats;
-      }, []);
-
-      if (!targetId) {
-        group.categories.push(moveCat);
-      }
-    } else {
-      group.categories = group.categories.filter(cat => cat.id !== id);
-    }
-
-    return { ...group };
-  });
-}
-
-export function moveCategoryGroup(categoryGroups, id, targetId) {
-  if (id === targetId) {
-    return categoryGroups;
-  }
-
-  const moveGroup = categoryGroups.find(g => g.id === id);
-
-  categoryGroups = categoryGroups.reduce((groups, group) => {
-    if (group.id === targetId) {
-      groups.push(moveGroup);
-      groups.push(group);
-    } else if (group.id !== id) {
-      groups.push(group);
-    }
-    return groups;
-  }, []);
-
-  if (!targetId) {
-    categoryGroups.push(moveGroup);
-  }
-
-  return categoryGroups;
-}
-
-export function deleteCategory(categoryGroups, id) {
-  return categoryGroups.map(group => {
-    group.categories = group.categories.filter(c => c.id !== id);
-    return group;
-  });
-}
-
-export function addGroup(categoryGroups, group) {
-  return [...categoryGroups, group];
-}
-
-export function updateGroup(categoryGroups, group) {
-  return categoryGroups.map(g => {
-    if (g.id === group.id) {
-      return { ...g, ...group };
-    }
-    return g;
-  });
-}
-
-export function deleteGroup(categoryGroups, id) {
-  return categoryGroups.filter(g => g.id !== id);
-}
diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts
index df63517dd8d4ae9d2116afec44e3e954a79a0e6d..251b6e5dd6ab619c0670ef517940f4f79ea839dc 100644
--- a/packages/loot-core/src/types/prefs.d.ts
+++ b/packages/loot-core/src/types/prefs.d.ts
@@ -53,6 +53,7 @@ export type LocalPrefs = Partial<
     reportsViewLegend: boolean;
     reportsViewSummary: boolean;
     reportsViewLabel: boolean;
+    'mobile.showSpentColumn': boolean;
   } & Record<`flags.${FeatureFlag}`, boolean>
 >;
 
diff --git a/upcoming-release-notes/2293.md b/upcoming-release-notes/2293.md
new file mode 100644
index 0000000000000000000000000000000000000000..d65818af8a0005770cd3e9d18d1371ff63dcb9e3
--- /dev/null
+++ b/upcoming-release-notes/2293.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [joel-jeremy]
+---
+
+Add hooks for frequently-made operations in the codebase.