Skip to content

Latest commit

 

History

History
146 lines (108 loc) · 6.21 KB

REACT_COMPILER.md

File metadata and controls

146 lines (108 loc) · 6.21 KB

React Compiler

What is the React Compiler?

React Compiler is a tool designed to enhance the performance of React applications by automatically memoizing components that lack optimizations.

At Expensify, we are early adopters of this tool and aim to fully leverage its capabilities.

React Compiler CI check

We have implemented a CI check that runs the React Compiler on all pull requests (PRs). This check compares compilable files from the PR branch with those in the target branch. If it detects that a file was previously compiled successfully but now fails to compile, the check will fail.

What if CI check fails in my PR?

If the CI check fails for your PR, you need to fix the problem. If you're unsure how to resolve it, you can ask for help in the #expensify-open-source Slack channel (and tag @Kiryl Ziusko).

How can I check what exactly prevents file from successful optimization or whether my fix for passing react-compiler actually works?

You can run npm run react-compiler-healthcheck and examine the output. This command will list the files that failed to compile and provide details on what caused the failures. The output can be extensive, so you may want to write it to a file for easier review:

npm run react-compiler-healthcheck &> output.txt

How to fix a particular problem?

Below are the most common failures and approaches to fix them:

New ref produces Mutating a value returned from a function whose return value should not be mutated

If you encounter this error, you need to add the Ref postfix to the variable name. For example:

-const rerender = useRef();
+const rerenderRef = useRef()

New SharedValue produces Mutating a value returned from a function whose return value should not be mutated

If you added a modification to SharedValue, you'll likely encounter this error. You can ignore this error for now because the current react-native-reanimated API is not compatible with react-compiler rules. Once this PR is merged, we'll rewrite the code to be compatible with react-compiler. Until then, you can ignore this error.

manual memoization could not be preserved

This error usually occurs when a dependency used inside a hook is omitted. This omission creates a memoization that is too complex to optimize automatically. Try including the missing dependencies.

Please be aware that react-compiler struggles with memoization of nested fields, i. e.:

// ❌ such code triggers the error
const selectedQboAccountName = useMemo(() => qboAccountOptions?.find(({id}) => id === qboConfig?.reimbursementAccountID)?.name, [qboAccountOptions, qboConfig?.reimbursementAccountID]);

// ✅ this code can be compiled successfully
const reimbursementAccountID = qboConfig?.reimbursementAccountID;
const selectedQboAccountName = useMemo(() => qboAccountOptions?.find(({id}) => id === reimbursementAccountID)?.name, [qboAccountOptions, reimbursementAccountID]);
// 👍 also new version of the code creates a variable for a repeated code
// which is great because it reduces the amount of the duplicated code

Invalid nesting in program blocks or scopes

Such error may happen if we have a nested memoization, i. e.:

const qboToggleSettingItems = [
    {
        onToggle: () => console.log('Hello world!'),
        subscribedSetting: CONST.QUICKBOOKS_CONFIG.ENABLED,
    },
];

return (
  <Container>
    {qboToggleSettingItems.map((item) => (
        <ToggleSettingOptionRow
            onToggle={item.onToggle}
            // ❌ such code triggers the error - `qboConfig?.pendingFields` is an external variable from the closure
            // so this code is pretty complicated for `react-compiler` optimizations 
            pendingAction={settingsPendingAction([item.subscribedSetting], qboConfig?.pendingFields)}
            // ❌ such code triggers the error - `qboConfig` is an external variable from the closure
            errors={ErrorUtils.getLatestErrorField(qboConfig, item.subscribedSetting)}
        />
    ))}
  </Container>
)

And below is a corrected version of the code:

const qboToggleSettingItems = [
    {
        onToggle: () => console.log('Hello world!'),
        subscribedSetting: CONST.QUICKBOOKS_CONFIG.ENABLED,
        // 👇 calculate variables and memoize `qboToggleSettingItems` object (done by `react-compiler`)
        errors: ErrorUtils.getLatestErrorField(qboConfig, CONST.QUICKBOOKS_CONFIG.ENABLED),
        pendingAction: settingsPendingAction([CONST.QUICKBOOKS_CONFIG.ENABLED], qboConfig?.pendingFields),
    },
];

return (
  <Container>
    {qboToggleSettingItems.map((item) => (
        <ToggleSettingOptionRow
            onToggle={item.onToggle}
            // ✅ we depend only on `qboToggleSettingItems`, no more complex closures, so everything is fine
            pendingAction={item.pendingAction}
            // ✅ we depend only on `qboToggleSettingItems`, no more complex closures, so everything is fine
            errors={item.errors}
        />
    ))}
  </Container>
)

Unexpected terminal kind optional for ternary test block

The problem happens when you have a ternary operator and you are using optional chaining ?. operator:

<OfflineWithFeedback
  description={menuItem.description}
  // ❌ such code triggers the error
  brickRoadIndicator={PolicyUtils.areSettingsInErrorFields(menuItem?.subscribedSettings, qboConfig?.errorFields) ? CONST.ERROR : undefined}
>
</OfflineWithFeedback>

In this case, qboConfig?.errorFields is causing the error, and the solution is to put it outside the ternary test block:

// 👇 move optional field outside of a ternary block
const errorFields = qboConfig?.errorFields;

...

<OfflineWithFeedback
    description={menuItem.description}
    // ✅ this code can be compiled successfully now
    brickRoadIndicator={PolicyUtils.areSettingsInErrorFields(menuItem?.subscribedSettings, errorFields) ? CONST.ERROR : undefined}
>
</OfflineWithFeedback>

What if my type of error is not listed here?

This list is actively maintained. If you discover a new error that is not listed and find a way to fix it, please update this documentation and create a PR.