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.
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.
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
Below are the most common failures and approaches to fix them:
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.
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
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>
)
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>
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.