fix: Resolve Require Cycles And LegendList Render Updates

This commit is contained in:
Rajarshee Chatterjee
2026-07-25 09:09:23 +05:30
parent 58db0352e6
commit 4de82472ee
12 changed files with 173 additions and 48 deletions
+62
View File
@@ -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(
+5 -2
View File
@@ -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)
+3
View File
@@ -29,3 +29,6 @@ onlyBuiltDependencies:
- better-sqlite3
- esbuild
- protobufjs
patchedDependencies:
'@legendapp/list@3.3.3': patches/@legendapp__list@3.3.3.patch
+1 -1
View File
@@ -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';
@@ -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 }) => <Text>{item}</Text>;
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(
<LegendList
data={['first']}
estimatedItemSize={40}
recycleItems={false}
renderItem={renderItem}
/>,
);
view.rerender(
<LegendList
data={[]}
estimatedItemSize={40}
recycleItems={false}
renderItem={renderItem}
/>,
);
view.rerender(
<LegendList
data={['second']}
estimatedItemSize={40}
recycleItems={false}
renderItem={renderItem}
/>,
);
expect(consoleError.mock.calls.flat().join(' ')).not.toContain(
'Cannot update a component',
);
});
});
+50
View File
@@ -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<TResult = any> {
toSQL(): Query;
all(): Promise<TResult[]>;
}
export function useLiveQuery<T extends ExecutableSelect>(
query: T,
fireOn: FireOn,
callback?: (data: Awaited<ReturnType<T['all']>>) => void,
) {
type ReturnValue = Awaited<ReturnType<T['all']>>;
const { sql: sqlString, params } = query.toSQL();
const paramsKey = JSON.stringify(params);
const fireOnKey = JSON.stringify(fireOn);
const callbackRef = useRef(callback);
const [data, setData] = useState<ReturnValue>(
() => 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;
}
+3 -39
View File
@@ -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<TResult = any> {
toSQL(): Query;
all(): Promise<TResult[]>; // Or TResult[] if you are using a synchronous driver
all(): Promise<TResult[]>;
get(): Promise<TResult | undefined>;
}
@@ -140,39 +139,4 @@ export const createDbManager = (dbInstance: DrizzleDb) => {
};
type TableNames = GetSelectTableName<Schema[keyof Schema]>;
type FireOn = { table: TableNames; ids?: number[] }[];
export function useLiveQuery<T extends ExecutableSelect>(
query: T,
fireOn: FireOn,
callback?: (data: Awaited<ReturnType<T['all']>>) => void,
) {
type ReturnValue = Awaited<ReturnType<T['all']>>;
const { sql: sqlString, params } = query.toSQL();
const paramsKey = JSON.stringify(params);
const fireOnKey = JSON.stringify(fireOn);
const cb = useRef(callback ?? (() => {}));
const [data, setData] = useState<ReturnValue>(() => {
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[] }[];
@@ -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';
@@ -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)}`;
@@ -0,0 +1 @@
export const BACKGROUND_TASKS_STORE_KEY = 'APP_SERVICE';
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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';