From 8498d7f78816edea6f48eec25b64232cdf4b41ee Mon Sep 17 00:00:00 2001
From: Matiss Janis Aboltins <matiss@mja.lv>
Date: Mon, 9 Sep 2024 08:04:41 +0100
Subject: [PATCH] :recycle: (synced-prefs) refactor number formatter away from
 redux (#3397)

---
 .../src/components/spreadsheet/useFormat.ts   | 36 +++++++++++++++----
 .../desktop-client/src/hooks/useDateFormat.ts |  7 ++--
 .../src/hooks/useFeatureFlag.ts               | 15 ++++----
 .../loot-core/src/client/reducers/prefs.ts    | 25 -------------
 packages/loot-core/src/client/selectors.ts    | 18 ----------
 .../src/client/state-types/prefs.d.ts         | 13 +++----
 packages/loot-core/src/shared/util.ts         |  2 +-
 upcoming-release-notes/3397.md                |  6 ++++
 8 files changed, 49 insertions(+), 73 deletions(-)
 delete mode 100644 packages/loot-core/src/client/selectors.ts
 create mode 100644 upcoming-release-notes/3397.md

diff --git a/packages/desktop-client/src/components/spreadsheet/useFormat.ts b/packages/desktop-client/src/components/spreadsheet/useFormat.ts
index f01c29a75..b5e13f6c5 100644
--- a/packages/desktop-client/src/components/spreadsheet/useFormat.ts
+++ b/packages/desktop-client/src/components/spreadsheet/useFormat.ts
@@ -1,8 +1,13 @@
-import { useCallback } from 'react';
-import { useSelector } from 'react-redux';
+import { useCallback, useEffect, useMemo } from 'react';
 
-import { selectNumberFormat } from 'loot-core/src/client/selectors';
-import { integerToCurrency } from 'loot-core/src/shared/util';
+import {
+  getNumberFormat,
+  integerToCurrency,
+  isNumberFormat,
+  setNumberFormat,
+} from 'loot-core/src/shared/util';
+
+import { useSyncedPref } from '../../hooks/useSyncedPref';
 
 export type FormatType =
   | 'string'
@@ -55,11 +60,28 @@ function format(
 }
 
 export function useFormat() {
-  const numberFormat = useSelector(selectNumberFormat);
+  const [numberFormat] = useSyncedPref('numberFormat');
+  const [hideFraction] = useSyncedPref('hideFraction');
+
+  const config = useMemo(
+    () => ({
+      format: isNumberFormat(numberFormat) ? numberFormat : 'comma-dot',
+      hideFraction: String(hideFraction) === 'true',
+    }),
+    [numberFormat, hideFraction],
+  );
+
+  // Hack: keep the global number format in sync - update the settings when
+  // the underlying configuration changes.
+  // This should be patched by moving all number-formatting utilities away from
+  // the global `getNumberFormat()` and to using the reactive `useFormat` hook.
+  useEffect(() => {
+    setNumberFormat(config);
+  }, [config]);
 
   return useCallback(
     (value: unknown, type: FormatType = 'string') =>
-      format(value, type, numberFormat.formatter),
-    [numberFormat],
+      format(value, type, getNumberFormat(config).formatter),
+    [config],
   );
 }
diff --git a/packages/desktop-client/src/hooks/useDateFormat.ts b/packages/desktop-client/src/hooks/useDateFormat.ts
index 258f156e1..9a4800ca2 100644
--- a/packages/desktop-client/src/hooks/useDateFormat.ts
+++ b/packages/desktop-client/src/hooks/useDateFormat.ts
@@ -1,7 +1,6 @@
-import { useSelector } from 'react-redux';
-
-import { type State } from 'loot-core/src/client/state-types';
+import { useSyncedPref } from './useSyncedPref';
 
 export function useDateFormat() {
-  return useSelector((state: State) => state.prefs.local?.dateFormat);
+  const [dateFormat] = useSyncedPref('dateFormat');
+  return dateFormat;
 }
diff --git a/packages/desktop-client/src/hooks/useFeatureFlag.ts b/packages/desktop-client/src/hooks/useFeatureFlag.ts
index 7bfd067d0..eb210ec47 100644
--- a/packages/desktop-client/src/hooks/useFeatureFlag.ts
+++ b/packages/desktop-client/src/hooks/useFeatureFlag.ts
@@ -1,8 +1,7 @@
-import { useSelector } from 'react-redux';
-
-import { type State } from 'loot-core/src/client/state-types';
 import type { FeatureFlag } from 'loot-core/src/types/prefs';
 
+import { useSyncedPref } from './useSyncedPref';
+
 const DEFAULT_FEATURE_FLAG_STATE: Record<FeatureFlag, boolean> = {
   reportBudget: false,
   goalTemplatesEnabled: false,
@@ -12,11 +11,9 @@ const DEFAULT_FEATURE_FLAG_STATE: Record<FeatureFlag, boolean> = {
 };
 
 export function useFeatureFlag(name: FeatureFlag): boolean {
-  return useSelector((state: State) => {
-    const value = state.prefs.local[`flags.${name}`];
+  const [value] = useSyncedPref(`flags.${name}`);
 
-    return value === undefined
-      ? DEFAULT_FEATURE_FLAG_STATE[name] || false
-      : String(value) === 'true';
-  });
+  return value === undefined
+    ? DEFAULT_FEATURE_FLAG_STATE[name] || false
+    : String(value) === 'true';
 }
diff --git a/packages/loot-core/src/client/reducers/prefs.ts b/packages/loot-core/src/client/reducers/prefs.ts
index 0d7496159..5523f5d55 100644
--- a/packages/loot-core/src/client/reducers/prefs.ts
+++ b/packages/loot-core/src/client/reducers/prefs.ts
@@ -1,5 +1,4 @@
 // @ts-strict-ignore
-import { isNumberFormat, setNumberFormat } from '../../shared/util';
 import * as constants from '../constants';
 import type { Action } from '../state-types';
 import type { PrefsState } from '../state-types/prefs';
@@ -12,32 +11,8 @@ const initialState: PrefsState = {
 export function update(state = initialState, action: Action): PrefsState {
   switch (action.type) {
     case constants.SET_PREFS:
-      if (action.prefs) {
-        setNumberFormat({
-          format: isNumberFormat(action.prefs.numberFormat)
-            ? action.prefs.numberFormat
-            : 'comma-dot',
-          hideFraction: String(action.prefs.hideFraction) === 'true',
-        });
-      }
       return { local: action.prefs, global: action.globalPrefs };
     case constants.MERGE_LOCAL_PREFS:
-      if (action.prefs.numberFormat || action.prefs.hideFraction != null) {
-        setNumberFormat({
-          format: isNumberFormat(action.prefs.numberFormat)
-            ? action.prefs.numberFormat
-            : isNumberFormat(state.local.numberFormat)
-              ? state.local.numberFormat
-              : 'comma-dot',
-          hideFraction:
-            String(
-              action.prefs.hideFraction != null
-                ? action.prefs.hideFraction
-                : state.local.hideFraction,
-            ) === 'true',
-        });
-      }
-
       return {
         ...state,
         local: { ...state.local, ...action.prefs },
diff --git a/packages/loot-core/src/client/selectors.ts b/packages/loot-core/src/client/selectors.ts
deleted file mode 100644
index ddb435689..000000000
--- a/packages/loot-core/src/client/selectors.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-// @ts-strict-ignore
-import { createSelector } from 'reselect';
-
-import { getNumberFormat, isNumberFormat } from '../shared/util';
-
-import type { State } from './state-types';
-
-const getState = (state: State) => state;
-
-const getPrefsState = createSelector(getState, state => state.prefs);
-const getLocalPrefsState = createSelector(getPrefsState, prefs => prefs.local);
-
-export const selectNumberFormat = createSelector(getLocalPrefsState, prefs =>
-  getNumberFormat({
-    format: isNumberFormat(prefs.numberFormat) ? prefs.numberFormat : undefined,
-    hideFraction: String(prefs.hideFraction) === 'true',
-  }),
-);
diff --git a/packages/loot-core/src/client/state-types/prefs.d.ts b/packages/loot-core/src/client/state-types/prefs.d.ts
index 275ee5532..7f125962f 100644
--- a/packages/loot-core/src/client/state-types/prefs.d.ts
+++ b/packages/loot-core/src/client/state-types/prefs.d.ts
@@ -1,25 +1,20 @@
-import type {
-  GlobalPrefs,
-  LocalPrefs,
-  MetadataPrefs,
-  SyncedPrefs,
-} from '../../types/prefs';
+import type { GlobalPrefs, LocalPrefs, MetadataPrefs } from '../../types/prefs';
 import type * as constants from '../constants';
 
 export type PrefsState = {
-  local: LocalPrefs & MetadataPrefs & SyncedPrefs;
+  local: LocalPrefs & MetadataPrefs;
   global: GlobalPrefs;
 };
 
 export type SetPrefsAction = {
   type: typeof constants.SET_PREFS;
-  prefs: LocalPrefs & MetadataPrefs & SyncedPrefs;
+  prefs: LocalPrefs & MetadataPrefs;
   globalPrefs: GlobalPrefs;
 };
 
 export type MergeLocalPrefsAction = {
   type: typeof constants.MERGE_LOCAL_PREFS;
-  prefs: LocalPrefs & MetadataPrefs & SyncedPrefs;
+  prefs: LocalPrefs & MetadataPrefs;
 };
 
 export type MergeGlobalPrefsAction = {
diff --git a/packages/loot-core/src/shared/util.ts b/packages/loot-core/src/shared/util.ts
index 879a8535f..43dabd753 100644
--- a/packages/loot-core/src/shared/util.ts
+++ b/packages/loot-core/src/shared/util.ts
@@ -233,7 +233,7 @@ const NUMBER_FORMATS = [
 
 type NumberFormats = (typeof NUMBER_FORMATS)[number];
 
-export function isNumberFormat(input: string): input is NumberFormats {
+export function isNumberFormat(input: string = ''): input is NumberFormats {
   return (NUMBER_FORMATS as readonly string[]).includes(input);
 }
 
diff --git a/upcoming-release-notes/3397.md b/upcoming-release-notes/3397.md
new file mode 100644
index 000000000..eb5ceb11f
--- /dev/null
+++ b/upcoming-release-notes/3397.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [MatissJanis]
+---
+
+SyncedPrefs: refactor usages of number formatter away from redux.
-- 
GitLab