Add refetchOnSync option to useTransactions to refetch when a server sync event is emitted (#6936)

* Migrate setupTests.js to TypeScript with proper types (#6871)

* Initial plan

* Rename setupTests.js to setupTests.ts and add proper types

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Extract Size type to avoid duplication

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Add release note for setupTests TypeScript migration

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Rename release note file to match PR number 6871

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Delete setupTests PR release note

* Add refetchOnSync to useTransactions to refetch when a server sync event is emitted

* Add release note for useTransactions refetchOnSync feature (#6937)

* Initial plan

* Add release note for PR 6936

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Coderabbit feedback

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>
This commit is contained in:
Joel Jeremy Marquez
2026-02-11 16:16:37 -08:00
committed by GitHub
parent 7fa9fa900b
commit 67d6592333
8 changed files with 64 additions and 120 deletions

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { listen, send } from 'loot-core/platform/client/fetch';
import { send } from 'loot-core/platform/client/fetch';
import type { Query } from 'loot-core/shared/query';
import { isPreviewId } from 'loot-core/shared/transactions';
import type { IntegerAmount } from 'loot-core/shared/util';
@@ -87,7 +87,6 @@ function TransactionListWithPreviews({
transactions,
runningBalances,
isPending: isTransactionsLoading,
refetch: reloadTransactions,
isFetchingNextPage: isLoadingMoreTransactions,
fetchNextPage: fetchMoreTransactions,
} = useTransactions({
@@ -129,21 +128,6 @@ function TransactionListWithPreviews({
}
}, [account.id, dispatch]);
useEffect(() => {
return listen('sync-event', event => {
if (event.type === 'applied') {
const tables = event.tables;
if (
tables.includes('transactions') ||
tables.includes('category_mapping') ||
tables.includes('payee_mapping')
) {
reloadTransactions();
}
}
});
}, [dispatch, reloadTransactions]);
const onOpenTransaction = useCallback(
(transaction: TransactionEntity) => {
if (!isPreviewId(transaction.id)) {

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { listen, send } from 'loot-core/platform/client/fetch';
import { send } from 'loot-core/platform/client/fetch';
import type { Query } from 'loot-core/shared/query';
import { isPreviewId } from 'loot-core/shared/transactions';
import type { TransactionEntity } from 'loot-core/types/models';
@@ -42,7 +42,6 @@ function TransactionListWithPreviews() {
const {
transactions,
isPending: isTransactionsLoading,
refetch: reloadTransactions,
isFetchingNextPage: isLoadingMoreTransactions,
fetchNextPage: fetchMoreTransactions,
} = useTransactions({
@@ -55,21 +54,6 @@ function TransactionListWithPreviews() {
const dispatch = useDispatch();
const navigate = useNavigate();
useEffect(() => {
return listen('sync-event', event => {
if (event.type === 'applied') {
const tables = event.tables;
if (
tables.includes('transactions') ||
tables.includes('category_mapping') ||
tables.includes('payee_mapping')
) {
reloadTransactions();
}
}
});
}, [reloadTransactions]);
const { isSearching, search: onSearch } = useTransactionsSearch({
updateQuery: setTransactionsQuery,
resetQuery: () => setTransactionsQuery(baseTransactionsQuery()),

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { listen, send } from 'loot-core/platform/client/fetch';
import { send } from 'loot-core/platform/client/fetch';
import type { Query } from 'loot-core/shared/query';
import { isPreviewId } from 'loot-core/shared/transactions';
import type { ScheduleEntity, TransactionEntity } from 'loot-core/types/models';
@@ -44,7 +44,6 @@ function TransactionListWithPreviews() {
const {
transactions,
isPending: isTransactionsLoading,
refetch: reloadTransactions,
isFetchingNextPage: isLoadingMoreTransactions,
fetchNextPage: fetchMoreTransactions,
} = useTransactions({
@@ -66,21 +65,6 @@ function TransactionListWithPreviews() {
const dispatch = useDispatch();
const navigate = useNavigate();
useEffect(() => {
return listen('sync-event', event => {
if (event.type === 'applied') {
const tables = event.tables;
if (
tables.includes('transactions') ||
tables.includes('category_mapping') ||
tables.includes('payee_mapping')
) {
reloadTransactions();
}
}
});
}, [reloadTransactions]);
const { isSearching, search: onSearch } = useTransactionsSearch({
updateQuery: setTransactionsQuery,
resetQuery: () => setTransactionsQuery(baseTransactionsQuery()),

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { listen, send } from 'loot-core/platform/client/fetch';
import { send } from 'loot-core/platform/client/fetch';
import type { Query } from 'loot-core/shared/query';
import { isPreviewId } from 'loot-core/shared/transactions';
import type { ScheduleEntity, TransactionEntity } from 'loot-core/types/models';
@@ -44,7 +44,6 @@ function TransactionListWithPreviews() {
const {
transactions,
isPending: isTransactionsLoading,
refetch: reloadTransactions,
isFetchingNextPage: isLoadingMoreTransactions,
fetchNextPage: fetchMoreTransactions,
} = useTransactions({
@@ -66,21 +65,6 @@ function TransactionListWithPreviews() {
const dispatch = useDispatch();
const navigate = useNavigate();
useEffect(() => {
return listen('sync-event', event => {
if (event.type === 'applied') {
const tables = event.tables;
if (
tables.includes('transactions') ||
tables.includes('category_mapping') ||
tables.includes('payee_mapping')
) {
reloadTransactions();
}
}
});
}, [reloadTransactions]);
const { isSearching, search: onSearch } = useTransactionsSearch({
updateQuery: setTransactionsQuery,
resetQuery: () => setTransactionsQuery(baseTransactionsQuery()),

View File

@@ -1,6 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { listen } from 'loot-core/platform/client/fetch';
import { q } from 'loot-core/shared/query';
import { isPreviewId } from 'loot-core/shared/transactions';
import type { CategoryEntity, TransactionEntity } from 'loot-core/types/models';
@@ -12,7 +11,6 @@ import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { useTransactions } from '@desktop-client/hooks/useTransactions';
import { useTransactionsSearch } from '@desktop-client/hooks/useTransactionsSearch';
import { useDispatch } from '@desktop-client/redux';
import * as bindings from '@desktop-client/spreadsheet/bindings';
type CategoryTransactionsProps = {
@@ -42,7 +40,6 @@ function TransactionListWithPreviews({
category,
month,
}: TransactionListWithPreviewsProps) {
const dispatch = useDispatch();
const navigate = useNavigate();
const baseTransactionsQuery = useCallback(
@@ -62,28 +59,12 @@ function TransactionListWithPreviews({
isPending: isTransactionsLoading,
isFetchingNextPage: isLoadingMoreTransactions,
fetchNextPage: fetchMoreTransactions,
refetch: reloadTransactions,
} = useTransactions({
query: transactionsQuery,
});
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
useEffect(() => {
return listen('sync-event', event => {
if (event.type === 'applied') {
const tables = event.tables;
if (
tables.includes('transactions') ||
tables.includes('category_mapping') ||
tables.includes('payee_mapping')
) {
reloadTransactions();
}
}
});
}, [dispatch, reloadTransactions]);
const { isSearching, search: onSearch } = useTransactionsSearch({
updateQuery: setTransactionsQuery,
resetQuery: () => setTransactionsQuery(baseTransactionsQuery()),

View File

@@ -1,6 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { listen } from 'loot-core/platform/client/fetch';
import { isPreviewId } from 'loot-core/shared/transactions';
import type { TransactionEntity } from 'loot-core/types/models';
@@ -11,11 +10,9 @@ import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { useTransactions } from '@desktop-client/hooks/useTransactions';
import { useTransactionsSearch } from '@desktop-client/hooks/useTransactionsSearch';
import { uncategorizedTransactions } from '@desktop-client/queries';
import { useDispatch } from '@desktop-client/redux';
import * as bindings from '@desktop-client/spreadsheet/bindings';
export function UncategorizedTransactions() {
const dispatch = useDispatch();
const navigate = useNavigate();
const baseTransactionsQuery = useCallback(
() => uncategorizedTransactions().options({ splits: 'inline' }).select('*'),
@@ -30,28 +27,12 @@ export function UncategorizedTransactions() {
isPending: isTransactionsLoading,
isFetchingNextPage: isLoadingMoreTransactions,
fetchNextPage: fetchMoreTransactions,
refetch: reloadTransactions,
} = useTransactions({
query: transactionsQuery,
});
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
useEffect(() => {
return listen('sync-event', event => {
if (event.type === 'applied') {
const tables = event.tables;
if (
tables.includes('transactions') ||
tables.includes('category_mapping') ||
tables.includes('payee_mapping')
) {
reloadTransactions();
}
}
});
}, [dispatch, reloadTransactions]);
const { search: onSearch } = useTransactionsSearch({
updateQuery: setTransactionsQuery,
resetQuery: () => setTransactionsQuery(baseTransactionsQuery()),

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useEffectEvent, useState } from 'react';
import { useInfiniteQuery } from '@tanstack/react-query';
import type {
@@ -6,9 +6,11 @@ import type {
UseInfiniteQueryResult,
} from '@tanstack/react-query';
import { listen } from 'loot-core/platform/client/fetch';
import type { Query } from 'loot-core/shared/query';
import type { IntegerAmount } from 'loot-core/shared/util';
import type { TransactionEntity } from 'loot-core/types/models';
import type { ServerEvents } from 'loot-core/types/server-events';
import { transactionQueries } from '@desktop-client/transactions';
@@ -62,6 +64,12 @@ type UseTransactionsProps = {
* @default 0
*/
startingBalance?: IntegerAmount;
/**
* Whether to refetch transactions when a sync event is emitted.
* @default true
*/
refetchOnSync?: boolean;
};
};
@@ -83,20 +91,46 @@ type UseTransactionsResult = UseInfiniteQueryResult<
export function useTransactions({
query,
options = { pageSize: 50, calculateRunningBalances: false },
options,
}: UseTransactionsProps): UseTransactionsResult {
const {
pageSize = 50,
calculateRunningBalances = false,
startingBalance,
refetchOnSync = true,
} = options ?? {};
const [runningBalances, setRunningBalances] = useState<
Map<TransactionEntity['id'], IntegerAmount>
>(new Map());
const queryResult = useInfiniteQuery(
transactionQueries.aql({ query, pageSize: options.pageSize }),
transactionQueries.aql({ query, pageSize }),
);
const onSyncEvent = useEffectEvent((event: ServerEvents['sync-event']) => {
if (event.type === 'applied') {
const tables = event.tables;
if (
tables.includes('transactions') ||
tables.includes('category_mapping') ||
tables.includes('payee_mapping')
) {
queryResult.refetch();
}
}
});
useEffect(() => {
if (!refetchOnSync) {
return;
}
return listen('sync-event', onSyncEvent);
}, [refetchOnSync]);
const calculateRunningBalancesOptionFn = getCalculateRunningBalancesFn(
options?.calculateRunningBalances,
calculateRunningBalances,
);
const startingBalanceOption = options?.startingBalance;
const splitsOption = query?.state.tableOptions
?.splits as TransactionSplitsOption;
@@ -108,12 +142,12 @@ export function useTransactions({
useEffect(() => {
if (calculateRunningBalancesOptionFn) {
if (queryResult.isSuccess) {
const transactions = queryResult.data.pages.flat();
const transactions = flattenPages(queryResult.data);
setRunningBalances(
calculateRunningBalancesOptionFn(
transactions,
splitsOption,
startingBalanceOption,
startingBalance,
),
);
}
@@ -124,13 +158,13 @@ export function useTransactions({
queryResult.data,
queryResult.isSuccess,
calculateRunningBalancesOptionFn,
startingBalanceOption,
startingBalance,
splitsOption,
]);
return {
...queryResult,
transactions: queryResult.data ? queryResult.data.pages.flat() : [],
transactions: flattenPages(queryResult.data),
runningBalances,
};
}
@@ -224,3 +258,9 @@ export function calculateRunningBalancesTopDown(
return acc;
}, new Map<TransactionEntity['id'], IntegerAmount>());
}
function flattenPages(
data?: InfiniteData<TransactionEntity[]>,
): TransactionEntity[] {
return data ? data.pages.flat() : [];
}

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [joel-jeremy]
---
Add refetchOnSync option to useTransactions hook to consolidate duplicate sync-event handling logic.