Skip to content

[No merge][NoQA] Test suite for "Convert code-inline-reviewer rules into proactive coding guidelines for Claude"#82206

Open
kacper-mikolajczak wants to merge 1 commit intoExpensify:mainfrom
callstack-internal:test/convert-ai-reviewer-rules-into-skill
Open

[No merge][NoQA] Test suite for "Convert code-inline-reviewer rules into proactive coding guidelines for Claude"#82206
kacper-mikolajczak wants to merge 1 commit intoExpensify:mainfrom
callstack-internal:test/convert-ai-reviewer-rules-into-skill

Conversation

@kacper-mikolajczak
Copy link
Contributor

Explanation of Change

The PR was created just to enable testing CI workflow of ai reviewer.

Fixed Issues

$
PROPOSAL:

Tests

  • Verify that no errors appear in the JS console

Offline tests

QA Steps

// TODO: These must be filled out, or the issue title must include "[No QA]."

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I verified there are no new alerts related to the canBeMissing param for useOnyx
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

@codecov
Copy link

codecov bot commented Feb 11, 2026

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
...c/components/__tests__/TestCrossFileViolations.tsx 0.00% <0.00%> (ø)
...mponents/__tests__/TestMultiViolationsSameRule.tsx 0.00% <0.00%> (ø)
src/components/__tests__/TestRuleViolations.tsx 0.00% <0.00%> (ø)
... and 248 files with indirect coverage changes

@kacper-mikolajczak kacper-mikolajczak changed the title [No merge] Test suite for "Convert code-inline-reviewer rules into proactive coding guidelines for Claude" [No merge][NoQA] Test suite for "Convert code-inline-reviewer rules into proactive coding guidelines for Claude" Feb 11, 2026
@kacper-mikolajczak kacper-mikolajczak marked this pull request as ready for review February 12, 2026 17:40
@kacper-mikolajczak kacper-mikolajczak requested review from a team as code owners February 12, 2026 17:40
@melvin-bot melvin-bot bot requested review from JmillsExpensify and MonilBhavsar and removed request for a team February 12, 2026 17:40
@melvin-bot
Copy link

melvin-bot bot commented Feb 12, 2026

@MonilBhavsar Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

1 similar comment
@melvin-bot
Copy link

melvin-bot bot commented Feb 12, 2026

@MonilBhavsar Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@kacper-mikolajczak
Copy link
Contributor Author

@JmillsExpensify @MonilBhavsar please disregard the reviewer request - this is a synthetic testing PR for AI reviewer improvement.

function MyCard(_props: {transaction: Transaction; onPress: () => void}) {
return null;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-1 (docs)

The spread operator {...item} creates a new object on every render iteration, forcing React to treat each item as changed and preventing reconciliation optimizations.

Fix: Pass props individually instead of spreading:

const renderItem = ({item}: {item: Item}) => <ItemRow id={item.id} reportID={item.reportID} value={item.value} highlight />;

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.


const renderItem = ({item}: {item: Item}) => <ItemRow {...item} highlight />;

function ExpensiveComponent({reportID}: {reportID?: string}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-2 (docs)

Expensive computation computeExpensiveLayout(data) occurs before validating reportID. If reportID is undefined, the expensive work is wasted.

Fix: Move the validation check before expensive operations:

function ExpensiveComponent({reportID}: {reportID?: string}) {
    if (!reportID) {
        return null;
    }
    const result = computeExpensiveLayout(data);
    return <View>{result}</View>;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

return <View>{result}</View>;
}

function ListItem({reportID}: {reportID: string}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-3 (docs)

Using useOnyx in a component rendered inside renderItem creates separate subscriptions for each list item, causing memory overhead and update cascades.

Fix: Use OnyxListItemProvider hooks instead:

function ListItem({reportID}: {reportID: string}) {
    const report = useReport(reportID);
    return <Text>{report?.reportName}</Text>;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
return <Text>{report?.reportName}</Text>;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-5 (docs)

Using deepEqual for the entire transaction object creates performance overhead that often exceeds the re-render cost. Compare only specific relevant properties.

Fix: Use shallow comparison of specific properties:

const MemoizedCard = memo(MyCard, (prev, next) => 
    prev.transaction.transactionID === next.transaction.transactionID && 
    prev.onPress === next.onPress
);

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

const MemoizedCard = memo(MyCard, (prev, next) => deepEqual(prev.transaction, next.transaction) && prev.onPress === next.onPress);

function Greeting({firstName, lastName}: Props) {
const [fullName, setFullName] = useState('');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-6 (docs)

Using useEffect to update state based on props causes an extra render pass and synchronization issues. The value can be computed directly.

Fix: Compute the value directly during rendering:

function Greeting({firstName, lastName}: Props) {
    const fullName = `${firstName} ${lastName}`;
    return <Text>{fullName}</Text>;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.


function CommentForm({userId}: {userId: string}) {
const [comment, setComment] = useState('');
const [rating, setRating] = useState(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-7 (docs)

Using useEffect to reset all component state when userId changes is not idiomatic. Use the key prop instead to trigger a full component reset.

Fix: Move the component reset to parent using key:

// In parent:
<CommentForm key={userId} userId={userId} />

// In component (no useEffect needed):
function CommentForm({userId}: {userId: string}) {
    const [comment, setComment] = useState(');
    const [rating, setRating] = useState(0);
    // Component resets automatically when userId changes due to key prop
    return <View>...</View>;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

}

function BuyButton({onBuy}: {onBuy: () => void}) {
const [isBuying, setIsBuying] = useState(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-8 (docs)

Using useEffect to respond to the user event (isBuying state change) adds unnecessary render cycles. Handle the action directly in the event handler.

Fix: Call the callback directly in the event handler:

function BuyButton({onBuy}: {onBuy: () => void}) {
    const handlePress = () => {
        onBuy();
    };
    return <Pressable onPress={handlePress} />;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.


function ChainedEffects({firstName, lastName}: Props) {
const [fullName, setFullName] = useState('');
const [isValid, setIsValid] = useState(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-9 (docs)

Multiple useEffect hooks form a chain where one effect's state update (setFullName) triggers another effect (setIsValid), creating unnecessary renders.

Fix: Compute derived values directly:

function ChainedEffects({firstName, lastName}: Props) {
    const fullName = `${firstName} ${lastName}`;
    const isValid = fullName.length > 0;
    return (
        <View>
            <Text>{fullName}</Text>
            <Text>{String(isValid)}</Text>
        </View>
    );
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

);
}

function Slider({value, onValueChange}: {value: number; onValueChange: (v: number) => void}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-10 (docs)

Using useEffect to call the parent callback onValueChange for communicating state changes violates React's unidirectional data flow pattern.

Fix: Lift state up to the parent component:

// In parent:
function Parent() {
    const [value, setValue] = useState(0);
    return <Slider value={value} onValueChange={setValue} />;
}

// In child:
function Slider({value, onValueChange}: {value: number; onValueChange: (v: number) => void}) {
    return <input value={value} onChange={(e) => onValueChange(Number(e.target.value))} />;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

}, [internal, onValueChange]);
return <Text>{internal}</Text>;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-11 (docs)

Using the entire PERSONAL_DETAILS_LIST object without a selector causes re-renders when any unrelated field changes, even though only currentUser.displayName is used.

Fix: Use a selector to access only the needed data:

function UserDisplayName() {
    const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {
        selector: (details) => details?.currentUser?.displayName,
    });
    return <Text>{personalDetails}</Text>;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

}

function Timer() {
const [count, setCount] = useState(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-12 (docs)

The setInterval is not cleared when the component unmounts, causing a memory leak and potential crashes in long-lived components.

Fix: Clear the interval in the cleanup function:

function Timer() {
    const [count, setCount] = useState(0);
    useEffect(() => {
        const intervalId = setInterval(() => setCount((c) => c + 1), 1000);
        return () => clearInterval(intervalId);
    }, []);
    return <Text>{count}</Text>;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.


function ItemList({items}: {items: Item[]}) {
return (
<View>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-13 (docs)

The getTheme() function call doesn't use the iterator variable and executes redundantly on every iteration, creating O(n) overhead.

Fix: Hoist the iterator-independent call outside the loop:

function ItemList({items}: {items: Item[]}) {
    const theme = getTheme();
    return (
        <View>
            {items.map((item) => (
                <Row key={item.id} color={theme.color} value={item.value} />
            ))}
        </View>
    );
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

</View>
);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-14 (docs)

Using useEffect + setState to mirror window.innerWidth misses React's concurrent rendering guarantees. useSyncExternalStore is purpose-built for this pattern.

Fix: Use useSyncExternalStore:

import {useSyncExternalStore} from 'react';

function subscribe(callback: () => void) {
    window.addEventListener('resize', callback);
    return () => window.removeEventListener('resize', callback);
}

function WindowWidth() {
    const width = useSyncExternalStore(
        subscribe,
        () => window.innerWidth,
        () => 0 // server snapshot
    );
    return <Text>{width}</Text>;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

return <Text>{width}</Text>;
}

function SearchResults({query}: {query: string}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-15 (docs)

The async operation fetchResults lacks cleanup, allowing race conditions where a slow earlier response can overwrite a faster later response when query changes rapidly.

Fix: Add an ignore flag to discard stale responses:

function SearchResults({query}: {query: string}) {
    const [results, setResults] = useState<never[]>([]);
    useEffect(() => {
        let ignore = false;
        fetchResults(query).then((json) => {
            if (!ignore) {
                setResults(json);
            }
        });
        return () => {
            ignore = true;
        };
    }, [query]);
    return <Text>{results.length}</Text>;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

}, [query]);
return <Text>{results.length}</Text>;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-16 (docs)

Non-idempotent initialization logic (loadDataFromLocalStorage, checkAuthToken) will execute twice in React Strict Mode, potentially causing auth token invalidation or duplicate operations.

Fix: Add a module-level guard:

let didInit = false;

function AppInit() {
    useEffect(() => {
        if (didInit) return;
        didInit = true;
        
        loadDataFromLocalStorage();
        checkAuthToken();
    }, []);
    return <View />;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

@MonilBhavsar
Copy link
Contributor

Looks like this does not need my review. Feel free to request, if it changes

@MonilBhavsar MonilBhavsar removed their request for review February 12, 2026 18:30

function DisplayName({first, last}: {first: string; last: string}) {
const [displayName, setDisplayName] = useState('');
useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test comment


function TotalDisplay({price, quantity}: {price: number; quantity: number}) {
const [total, setTotal] = useState(0);
useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-6 (docs)

total is directly derivable from price * quantity — no state or effect needed. This causes an unnecessary extra render cycle on every prop change.

Compute the value inline instead:

function TotalDisplay({price, quantity}: {price: number; quantity: number}) {
    const total = price * quantity;
    return <Text>{total}</Text>;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

return null;
}

const renderItem = ({item}: {item: Item}) => <ItemRow {...item} highlight />;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-1 (docs)

Spreading ...item inside a renderItem function creates a new object on every render, preventing React reconciliation optimizations and causing unnecessary re-renders for every list item.

Pass individual props explicitly instead:

const renderItem = ({item}: {item: Item}) => (
    <ItemRow id={item.id} reportID={item.reportID} value={item.value} highlight />
);

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

const renderItem = ({item}: {item: Item}) => <ItemRow {...item} highlight />;

function ExpensiveComponent({reportID}: {reportID?: string}) {
const result = computeExpensiveLayout(data);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-2 (docs)

computeExpensiveLayout(data) executes before the !reportID guard check on line 59. If reportID is falsy, this expensive computation is wasted.

Move the early return before the expensive call:

function ExpensiveComponent({reportID}: {reportID?: string}) {
    if (!reportID) {
        return null;
    }
    const result = computeExpensiveLayout(data);
    return <View>{result}</View>;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

return <Text>{report?.reportName}</Text>;
}

const MemoizedCard = memo(MyCard, (prev, next) => deepEqual(prev.transaction, next.transaction) && prev.onPress === next.onPress);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-5 (docs)

deepEqual(prev.transaction, next.transaction) in React.memo recursively compares all nested properties, creating performance overhead that often exceeds the re-render cost it aims to prevent.

Compare only the specific relevant properties instead:

const MemoizedCard = memo(MyCard, (prev, next) =>
    prev.transaction.transactionID === next.transaction.transactionID &&
    prev.onPress === next.onPress
);

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.


function Greeting({firstName, lastName}: Props) {
const [fullName, setFullName] = useState('');
useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-6 (docs)

fullName is directly derivable from firstName and lastName props — no state or effect needed. This causes an unnecessary extra render cycle on every prop change.

Compute the value inline instead:

function Greeting({firstName, lastName}: Props) {
    const fullName = `${firstName} ${lastName}`;
    return <Text>{fullName}</Text>;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

function CommentForm({userId}: {userId: string}) {
const [comment, setComment] = useState('');
const [rating, setRating] = useState(0);
useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-7 (docs)

This useEffect resets all component state (comment and rating) when userId changes. This is the exact pattern where the key prop should be used instead — React will unmount and remount the component, automatically resetting all state.

Use a key prop on the parent instead:

// In the parent component:
<CommentForm key={userId} userId={userId} />

// In CommentForm, remove the useEffect entirely:
function CommentForm({userId}: {userId: string}) {
    const [comment, setComment] = useState("");
    const [rating, setRating] = useState(0);
    // State resets automatically when key changes
    return (...);
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.


function BuyButton({onBuy}: {onBuy: () => void}) {
const [isBuying, setIsBuying] = useState(false);
useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-8 (docs)

The buy action is a user event that should be handled directly in the event handler, not routed through state and a useEffect. This adds an unnecessary render cycle and makes the relationship between user action and response less clear.

Handle the event directly in the press handler:

function BuyButton({onBuy}: {onBuy: () => void}) {
    return <Pressable onPress={() => onBuy()} />;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

function ChainedEffects({firstName, lastName}: Props) {
const [fullName, setFullName] = useState('');
const [isValid, setIsValid] = useState(false);
useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-9 (docs)

These two useEffects form a chain: the first sets fullName, which triggers the second to set isValid. Both values are directly derivable from props and should be computed inline.

Compute derived values directly in the component body:

function ChainedEffects({firstName, lastName}: Props) {
    const fullName = `${firstName} ${lastName}`;
    const isValid = fullName.length > 0;
    return (
        <View>
            <Text>{fullName}</Text>
            <Text>{String(isValid)}</Text>
        </View>
    );
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.


function Slider({value, onValueChange}: {value: number; onValueChange: (v: number) => void}) {
const [internal, setInternal] = useState(value);
useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-10 (docs)

This useEffect calls the parent callback onValueChange whenever internal state changes. Parent-child communication should not use useEffect. Instead, lift the state up to the parent and pass it down as props.

Lift state to the parent:

// Parent owns the state:
function Parent() {
    const [value, setValue] = useState(0);
    return <Slider value={value} onChange={setValue} />;
}

// Slider becomes a controlled component — no internal state or effect needed
function Slider({value, onChange}: {value: number; onChange: (v: number) => void}) {
    return <Text>{value}</Text>;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

}

function UserDisplayName() {
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-11 (docs)

useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST) subscribes to the entire personal details collection, but only currentUser?.displayName is used. The component will re-render whenever any field in the personal details list changes.

Use a selector to subscribe only to the needed data:

const [displayName] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {
    selector: (details) => details?.currentUser?.displayName,
});
return <Text>{displayName}</Text>;

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.


function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-12 (docs)

setInterval is created without a cleanup function. The interval will continue running after the component unmounts, causing a memory leak and attempting to update state on an unmounted component.

Add a cleanup function to clear the interval:

useEffect(() => {
    const intervalId = setInterval(() => setCount((c) => c + 1), 1000);
    return () => clearInterval(intervalId);
}, []);

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

return (
<View>
{items.map((item) => {
const theme = getTheme();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-13 (docs)

getTheme() is called on every iteration of .map() but does not depend on the iterator variable item. It produces the same result each time, creating O(n) redundant calls.

Hoist the call outside the loop:

function ItemList({items}: {items: Item[]}) {
    const theme = getTheme();
    return (
        <View>
            {items.map((item) => (
                <Row key={item.id} color={theme.color} value={item.value} />
            ))}
        </View>
    );
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.


function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-14 (docs)

This useEffect manually subscribes to the resize DOM event and mirrors window.innerWidth into state via setWidth. This is the exact pattern useSyncExternalStore was designed for — it handles concurrent rendering edge cases and avoids the extra render pass.

Use useSyncExternalStore instead:

function subscribe(callback: () => void) {
    window.addEventListener("resize", callback);
    return () => window.removeEventListener("resize", callback);
}

function WindowWidth() {
    const width = useSyncExternalStore(
        subscribe,
        () => window.innerWidth,
    );
    return <Text>{width}</Text>;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.


function SearchResults({query}: {query: string}) {
const [results, setResults] = useState<never[]>([]);
useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-15 (docs)

This useEffect fetches data based on query and calls setResults without any cleanup mechanism. If query changes rapidly, a slow earlier response can overwrite the result of a faster later response, showing stale data.

Add an ignore flag to discard stale responses:

useEffect(() => {
    let ignore = false;
    fetchResults(query).then((json) => {
        if (\!ignore) {
            setResults(json);
        }
    });
    return () => { ignore = true; };
}, [query]);

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

}

function AppInit() {
useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-16 (docs)

loadDataFromLocalStorage() and checkAuthToken() are non-idempotent initialization calls in a useEffect([]) without any execution guard. React Strict Mode will double-invoke this effect in development, potentially causing issues like invalidated tokens.

Add a module-level guard:

let didInit = false;

function AppInit() {
    useEffect(() => {
        if (didInit) {
            return;
        }
        didInit = true;
        loadDataFromLocalStorage();
        checkAuthToken();
    }, []);
    return <View />;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

}

function PlatformButton() {
const isAndroid = Platform.OS === 'android';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CONSISTENCY-1 (docs)

Platform.OS === "android" is used to conditionally render different components inline. This mixes platform-specific logic within the component, increasing maintenance overhead and complexity.

Separate platform-specific implementations into dedicated files using file extensions:

PlatformButton.android.tsx  // TouchableOpacity version
PlatformButton.ios.tsx      // Pressable version

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

}

function RetryLogic() {
if (attempts < 3) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CONSISTENCY-2 (docs)

3 and 5000 are magic numbers without documentation or named constants. Their purpose is unclear without context.

Extract them into named constants:

const MAX_RETRY_ATTEMPTS = 3;
const API_TIMEOUT_MS = 5000;

function RetryLogic() {
    if (attempts < MAX_RETRY_ATTEMPTS) {
        return apiCall({timeout: API_TIMEOUT_MS});
    }
    return null;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

return null;
}

function formatCurrencyA(amount: number) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CONSISTENCY-3 (docs)

formatCurrencyA and formatCurrencyB (line 204) are identical functions performing the same currency formatting. This violates the DRY principle and doubles maintenance burden.

Consolidate into a single reusable function:

function formatCurrency(amount: number) {
    return new Intl.NumberFormat("en-US", {style: "currency", currency: "USD"}).format(amount);
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

return new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD'}).format(amount);
}

type BadProps = {title: string; onPress: () => void; unusedProp: string};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CONSISTENCY-4 (docs)

unusedProp is defined in BadProps but never used in the BadButton component. This increases component complexity and confuses the API surface.

Remove the unused prop from the type:

type BadProps = {title: string; onPress: () => void};

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

}

function UnjustifiedDisable() {
// eslint-disable-next-line react-hooks/exhaustive-deps
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CONSISTENCY-5 (docs)

eslint-disable-next-line react-hooks/exhaustive-deps is used without an accompanying comment explaining why the rule is being disabled.

Add a justification comment:

// eslint-disable-next-line react-hooks/exhaustive-deps -- This effect should only run on mount; dependencies are intentionally omitted
useEffect(() => {
    doSomething();
}, []);

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

return <View />;
}

async function submitExpense(expenseData: ExpenseData) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CONSISTENCY-6 (docs)

API.submit(expenseData) is an async operation that can fail, but there is no error handling. If the submission fails, showSuccessMessage() would not execute but the error would be unhandled and the user would receive no feedback.

Add try/catch with proper error handling:

async function submitExpense(expenseData: ExpenseData) {
    try {
        await API.submit(expenseData);
        showSuccessMessage();
    } catch (error) {
        Log.error("Expense submission failed", error);
        showErrorMessage("Failed to submit expense. Please try again.");
    }
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

showSuccessMessage();
}

type ConfigTableProps = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CLEAN-REACT-PATTERNS-1 (docs)

ConfigTable uses boolean configuration props (shouldShowSearchBar, shouldShowHeader, shouldEnableSorting, shouldShowPagination) to control feature presence. Adding new features requires modifying this component API.

Use composition instead — express features as composable children:

<ConfigTable>
    <ConfigTable.SearchBar />
    <ConfigTable.Header sortable />
    <ConfigTable.Body />
    <ConfigTable.Pagination />
</ConfigTable>

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

);
}

function ReportParent({reportID}: {reportID: string}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CLEAN-REACT-PATTERNS-2 (docs)

ReportParent fetches report, policy, and transaction via useOnyx only to pass them as props to ReportChild. The parent is a pure data intermediary — it does not use the data for its own rendering or logic. If ReportChild requirements change, the parent must change too.

Let ReportChild own its data by passing only the ID:

function ReportParent({reportID}: {reportID: string}) {
    return <ReportChild reportID={reportID} />;
}

// ReportChild fetches its own data
function ReportChild({reportID}: {reportID: string}) {
    const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
    const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`);
    const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${report?.transactionID}`);
    // ...
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.


type WaypointListProps = {
transaction: Transaction;
navigateToWaypointEditPage: (index: number) => void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CLEAN-REACT-PATTERNS-3 (docs)

navigateToWaypointEditPage: (index: number) => void encodes a consumer-specific routing assumption into the component contract. The transaction prop is also passed primarily to compute the index for this callback, not for rendering purposes. This couples WaypointList to a specific navigation flow.

Use an abstract callback that describes what the component can do, not what the consumer should do:

type WaypointListProps = {
    waypoints: Array<{id: string; name: string}>;
    onSelectWaypoint: (waypoint: {id: string; name: string}) => void;
};

function WaypointList({waypoints, onSelectWaypoint}: WaypointListProps) {
    return (
        <View>
            {waypoints.map((wp) => (
                <Pressable key={wp.id} onPress={() => onSelectWaypoint(wp)}>
                    <Text>{wp.name}</Text>
                </Pressable>
            ))}
        </View>
    );
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

);
}

function KitchenSink() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CLEAN-REACT-PATTERNS-4 (docs)

KitchenSink aggregates five unrelated concerns in a single component: page view tracking, deep link handling, audio session setup, session checking, and navigation prefetching. Each concern is interleaved via separate useEffects, making it hard to modify one without risking regression in another.

Extract each concern into a focused custom hook or separate component:

function KitchenSink() {
    usePageViewTracking();
    useDeepLinkHandler();
    useAudioSession();
    useSessionCheck();
    useNavigationPrefetch();
    return <View />;
}

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

}

const AppContext = React.createContext({});
function AppProvider({children}: {children: React.ReactNode}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CLEAN-REACT-PATTERNS-5 (docs)

AppProvider bundles four unrelated Onyx subscriptions (priorityMode, chatReports, policies, transactions) into a single context value. Any consumer that only needs priorityMode will re-render when chatReports, policies, or transactions change — these are independent data domains that change at different frequencies.

Split into cohesive, focused contexts:

// Each provider owns a cohesive slice of state
function PriorityModeProvider({children}: {children: React.ReactNode}) {
    const [priorityMode] = useOnyx(ONYXKEYS.NVP_PRIORITY_MODE);
    return <PriorityModeContext.Provider value={priorityMode}>{children}</PriorityModeContext.Provider>;
}

// Or let consumers subscribe directly via useOnyx where needed

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

Copy link
Contributor

@JmillsExpensify JmillsExpensify left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Product review not relevant on this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants