From 4de82472ee70fd0c36114f0ee2e213ff6bf81099 Mon Sep 17 00:00:00 2001 From: Rajarshee Chatterjee Date: Sat, 25 Jul 2026 09:09:23 +0530 Subject: [PATCH] fix: Resolve Require Cycles And LegendList Render Updates --- patches/@legendapp__list@3.3.3.patch | 62 +++++++++++++++++++ pnpm-lock.yaml | 7 ++- pnpm-workspace.yaml | 3 + src/components/SearchbarV2/SearchbarV2.tsx | 2 +- .../__tests__/LegendListPatch.test.tsx | 43 +++++++++++++ src/database/manager/liveQuery.ts | 50 +++++++++++++++ src/database/manager/manager.ts | 42 +------------ .../SettingsRepositoryScreen.tsx | 2 +- .../backgroundTasks/BackgroundTaskQueue.ts | 3 +- src/services/backgroundTasks/constants.ts | 1 + src/services/backgroundTasks/index.ts | 2 +- src/services/backup/utils.ts | 4 +- 12 files changed, 173 insertions(+), 48 deletions(-) create mode 100644 patches/@legendapp__list@3.3.3.patch create mode 100644 src/components/__tests__/LegendListPatch.test.tsx create mode 100644 src/database/manager/liveQuery.ts create mode 100644 src/services/backgroundTasks/constants.ts diff --git a/patches/@legendapp__list@3.3.3.patch b/patches/@legendapp__list@3.3.3.patch new file mode 100644 index 00000000..914356bb --- /dev/null +++ b/patches/@legendapp__list@3.3.3.patch @@ -0,0 +1,62 @@ +diff --git a/react-native.js b/react-native.js +index 229f09a8230ab24079d471f65d28d36908f0f2ea..5c9a6c2501d8de3c44e138593831f3442b4b9204 100644 +--- a/react-native.js ++++ b/react-native.js +@@ -7256,15 +7256,17 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + useWindowScroll: useWindowScrollResolved + }; + state.refScroller = refScroller; +- if (!isFirstLocal && previousAdaptiveRender && !experimental_adaptiveRender) { +- resetAdaptiveRender(ctx); +- } +- if (shouldResetFreshDataLayout) { +- resetInitialRenderState(ctx, { +- resetInitialScroll: !!initialScrollProp, +- resetLayout: true +- }); +- } ++ React2.useLayoutEffect(() => { ++ if (!isFirstLocal && previousAdaptiveRender && !experimental_adaptiveRender) { ++ resetAdaptiveRender(ctx); ++ } ++ if (shouldResetFreshDataLayout) { ++ resetInitialRenderState(ctx, { ++ resetInitialScroll: !!initialScrollProp, ++ resetLayout: true ++ }); ++ } ++ }, [experimental_adaptiveRender, initialScrollProp, isFirstLocal, previousAdaptiveRender, shouldResetFreshDataLayout]); + const memoizedLastItemKeys = React2.useMemo(() => { + if (!dataProp.length) return []; + return Array.from( +diff --git a/react-native.mjs b/react-native.mjs +index c2e0f3823f524e31e2105a5fbda76be78c926226..dc0c9c6d5b887968c631dd0ba1646fd40ba28c63 100644 +--- a/react-native.mjs ++++ b/react-native.mjs +@@ -7235,15 +7235,17 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + useWindowScroll: useWindowScrollResolved + }; + state.refScroller = refScroller; +- if (!isFirstLocal && previousAdaptiveRender && !experimental_adaptiveRender) { +- resetAdaptiveRender(ctx); +- } +- if (shouldResetFreshDataLayout) { +- resetInitialRenderState(ctx, { +- resetInitialScroll: !!initialScrollProp, +- resetLayout: true +- }); +- } ++ useLayoutEffect(() => { ++ if (!isFirstLocal && previousAdaptiveRender && !experimental_adaptiveRender) { ++ resetAdaptiveRender(ctx); ++ } ++ if (shouldResetFreshDataLayout) { ++ resetInitialRenderState(ctx, { ++ resetInitialScroll: !!initialScrollProp, ++ resetLayout: true ++ }); ++ } ++ }, [experimental_adaptiveRender, initialScrollProp, isFirstLocal, previousAdaptiveRender, shouldResetFreshDataLayout]); + const memoizedLastItemKeys = useMemo(() => { + if (!dataProp.length) return []; + return Array.from( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13f727b8..0b4ff1e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +patchedDependencies: + '@legendapp/list@3.3.3': 37a9c8d7d9c1bd33c957ba6ea78291c7b08532dc32de4925f3d612cb41f1a659 + importers: .: @@ -16,7 +19,7 @@ importers: version: 5.2.14(8883bda3e7ffd4622bceabc44263e41c) '@legendapp/list': specifier: ^3.3.3 - version: 3.3.3(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3) + version: 3.3.3(patch_hash=37a9c8d7d9c1bd33c957ba6ea78291c7b08532dc32de4925f3d612cb41f1a659)(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3) '@noble/ciphers': specifier: ^2.2.0 version: 2.2.0 @@ -7559,7 +7562,7 @@ snapshots: dependencies: jsbi: 4.3.2 - '@legendapp/list@3.3.3(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3)': + '@legendapp/list@3.3.3(patch_hash=37a9c8d7d9c1bd33c957ba6ea78291c7b08532dc32de4925f3d612cb41f1a659)(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3)': dependencies: react: 19.2.3 use-sync-external-store: 1.6.0(react@19.2.3) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 763c1c77..5272b6ed 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -29,3 +29,6 @@ onlyBuiltDependencies: - better-sqlite3 - esbuild - protobufjs + +patchedDependencies: + '@legendapp/list@3.3.3': patches/@legendapp__list@3.3.3.patch diff --git a/src/components/SearchbarV2/SearchbarV2.tsx b/src/components/SearchbarV2/SearchbarV2.tsx index 12ef5b15..bdcafca0 100644 --- a/src/components/SearchbarV2/SearchbarV2.tsx +++ b/src/components/SearchbarV2/SearchbarV2.tsx @@ -3,7 +3,7 @@ import { StyleSheet, View, TextInput } from 'react-native'; import IconButtonV2 from '../IconButtonV2/IconButtonV2'; import { ThemeColors } from '../../theme/types'; -import { Menu } from '@components'; +import Menu from '../Menu'; import { MaterialDesignIconName } from '@type/icon'; import { getString } from '@i18n/translations'; import { Pressable } from 'react-native-gesture-handler'; diff --git a/src/components/__tests__/LegendListPatch.test.tsx b/src/components/__tests__/LegendListPatch.test.tsx new file mode 100644 index 00000000..3f84601a --- /dev/null +++ b/src/components/__tests__/LegendListPatch.test.tsx @@ -0,0 +1,43 @@ +import { render } from '@testing-library/react-native'; +import { Text } from 'react-native'; +import { LegendList } from '@legendapp/list/react-native'; + +const renderItem = ({ item }: { item: string }) => {item}; + +describe('LegendList render lifecycle patch', () => { + it('does not update its container layer while rendering fresh data', () => { + const consoleError = jest + .spyOn(console, 'error') + .mockImplementation(() => undefined); + + const view = render( + , + ); + + view.rerender( + , + ); + view.rerender( + , + ); + + expect(consoleError.mock.calls.flat().join(' ')).not.toContain( + 'Cannot update a component', + ); + }); +}); diff --git a/src/database/manager/liveQuery.ts b/src/database/manager/liveQuery.ts new file mode 100644 index 00000000..cd65b5a5 --- /dev/null +++ b/src/database/manager/liveQuery.ts @@ -0,0 +1,50 @@ +import { useEffect, useRef, useState } from 'react'; +import type { Query } from 'drizzle-orm'; + +import { db } from '@database/db'; +import type { FireOn } from './manager'; + +interface ExecutableSelect { + toSQL(): Query; + all(): Promise; +} + +export function useLiveQuery( + query: T, + fireOn: FireOn, + callback?: (data: Awaited>) => void, +) { + type ReturnValue = Awaited>; + + const { sql: sqlString, params } = query.toSQL(); + const paramsKey = JSON.stringify(params); + const fireOnKey = JSON.stringify(fireOn); + const callbackRef = useRef(callback); + + const [data, setData] = useState( + () => db.executeSync(sqlString, params as any[]).rows as ReturnValue, + ); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + useEffect(() => { + callbackRef.current?.(data); + }, [data]); + + useEffect(() => { + const unsubscribe = db.reactiveExecute({ + query: sqlString, + arguments: params as any[], + fireOn, + callback: (result: { rows: ReturnValue }) => { + setData(result.rows); + }, + }); + return unsubscribe; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sqlString, paramsKey, fireOnKey]); + + return data; +} diff --git a/src/database/manager/manager.ts b/src/database/manager/manager.ts index 2d81003a..cc5690d7 100644 --- a/src/database/manager/manager.ts +++ b/src/database/manager/manager.ts @@ -1,9 +1,8 @@ -import { db, drizzleDb } from '@database/db'; +import type { drizzleDb } from '@database/db'; import type { SQLBatchTuple, Scalar } from '@op-engineering/op-sqlite'; import { IDbManager } from './manager.d'; import { DbTaskQueue } from './queue'; import { Schema } from '../schema'; -import { useEffect, useRef, useState } from 'react'; import { GetSelectTableName } from 'drizzle-orm/query-builders/select.types'; import { AnyColumn, @@ -21,7 +20,7 @@ type TransactionParameter = Parameters< interface ExecutableSelect { toSQL(): Query; - all(): Promise; // Or TResult[] if you are using a synchronous driver + all(): Promise; get(): Promise; } @@ -140,39 +139,4 @@ export const createDbManager = (dbInstance: DrizzleDb) => { }; type TableNames = GetSelectTableName; -type FireOn = { table: TableNames; ids?: number[] }[]; - -export function useLiveQuery( - query: T, - fireOn: FireOn, - callback?: (data: Awaited>) => void, -) { - type ReturnValue = Awaited>; - - const { sql: sqlString, params } = query.toSQL(); - const paramsKey = JSON.stringify(params); - const fireOnKey = JSON.stringify(fireOn); - const cb = useRef(callback ?? (() => {})); - - const [data, setData] = useState(() => { - const r = db.executeSync(sqlString, params as any[]).rows as ReturnValue; - cb.current(r); - return r; - }); - - useEffect(() => { - const unsub = db.reactiveExecute({ - query: sqlString, - arguments: params as any[], - fireOn, - callback: (result: { rows: ReturnValue }) => { - setData(result.rows); - cb.current(result.rows); - }, - }); - return unsub; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sqlString, paramsKey, fireOnKey]); - - return data; -} +export type FireOn = { table: TableNames; ids?: number[] }[]; diff --git a/src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx b/src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx index f5af05ef..67fe781b 100644 --- a/src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx +++ b/src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx @@ -22,7 +22,7 @@ import { RootStackParamList, } from '@navigators/types'; import { showToast } from '@utils/showToast'; -import { useLiveQuery } from '@database/manager/manager'; +import { useLiveQuery } from '@database/manager/liveQuery'; import { repositorySchema } from '@database/schema'; import { dbManager } from '@database/db'; diff --git a/src/services/backgroundTasks/BackgroundTaskQueue.ts b/src/services/backgroundTasks/BackgroundTaskQueue.ts index c7526645..04d8169a 100644 --- a/src/services/backgroundTasks/BackgroundTaskQueue.ts +++ b/src/services/backgroundTasks/BackgroundTaskQueue.ts @@ -17,8 +17,7 @@ import { createBackgroundTaskMetadata, fromNativeTaskRecord, } from './taskDefinitions'; - -export const BACKGROUND_TASKS_STORE_KEY = 'APP_SERVICE'; +import { BACKGROUND_TASKS_STORE_KEY } from './constants'; const makeTemporaryId = () => `pending-${Date.now()}-${Math.random().toString(36).slice(2)}`; diff --git a/src/services/backgroundTasks/constants.ts b/src/services/backgroundTasks/constants.ts new file mode 100644 index 00000000..da9ecce6 --- /dev/null +++ b/src/services/backgroundTasks/constants.ts @@ -0,0 +1 @@ +export const BACKGROUND_TASKS_STORE_KEY = 'APP_SERVICE'; diff --git a/src/services/backgroundTasks/index.ts b/src/services/backgroundTasks/index.ts index 8e882c9f..dff624f1 100644 --- a/src/services/backgroundTasks/index.ts +++ b/src/services/backgroundTasks/index.ts @@ -1,5 +1,5 @@ export { backgroundTasks } from './backgroundTasks'; -export { BACKGROUND_TASKS_STORE_KEY } from './BackgroundTaskQueue'; +export { BACKGROUND_TASKS_STORE_KEY } from './constants'; export { runHeadlessBackgroundTask } from './headlessTask'; export type { BackgroundTask, diff --git a/src/services/backup/utils.ts b/src/services/backup/utils.ts index 6f962892..27ff6058 100644 --- a/src/services/backup/utils.ts +++ b/src/services/backup/utils.ts @@ -16,8 +16,8 @@ import { import { BackupCategory, BackupNovel } from '@database/types'; import { BackupEntryName } from './types'; import { ROOT_STORAGE } from '@utils/Storages'; -import { BACKGROUND_TASKS_STORE_KEY } from '@services/backgroundTasks/BackgroundTaskQueue'; -import NativeFile from '@modules/native-file' +import { BACKGROUND_TASKS_STORE_KEY } from '@services/backgroundTasks/constants'; +import NativeFile from '@modules/native-file'; import { showToast } from '@utils/showToast'; import { getString } from '@i18n/translations';