Skip to content

Ancestor validation doesn't work across file boundaries #1

@mfkrause

Description

@mfkrause

In order to optimize View components, we need to ensure that they are not wrapped inside a Text component. Currently, we only check within the current file's context, if there's a Text ancestor component (direct or indirect, i.e. within a custom component – as long as it's defined within the same file):

/**
* Returns true if any ancestor element is a <Text /> or contains a <Text />.
* This function handles both direct Text ancestors and custom components that may contain Text.
* TODO: We can't test across file boundaries within the Babel plugin
*/
function hasTextAncestor(path: NodePath<t.JSXOpeningElement>): boolean {
// Check for direct Text ancestors (no custom components)
const directTextAncestor = path.findParent((parentPath) => {
return t.isJSXElement(parentPath.node) && t.isJSXIdentifier(parentPath.node.openingElement.name, { name: 'Text' });
});
if (directTextAncestor) return true;
// Check for indirect Text ancestors (custom components that contain Text)
return !!path.findParent((parentPath) => {
// Only check JSX elements
if (!t.isJSXElement(parentPath.node)) return false;
// Get the component name
const openingElement = parentPath.node.openingElement;
if (!t.isJSXIdentifier(openingElement.name)) return false;
const componentName = openingElement.name.name;
// Skip built-in components and already checked Text component
if (
componentName === 'Text' ||
componentName === 'View' ||
componentName === 'Fragment' ||
componentName[0] === componentName[0].toLowerCase()
) {
return false;
}
// Try to find the component definition through variable binding
const binding = parentPath.scope.getBinding(componentName);
if (!binding) return false;
// Now check the component definition for Text elements
if (t.isVariableDeclarator(binding.path.node)) {
const init = binding.path.node.init;
// Handle arrow functions or function expressions
if (t.isArrowFunctionExpression(init) || t.isFunctionExpression(init)) {
// Check the function body for Text elements
return t.isBlockStatement(init.body) ? hasTextInReturnStatement(init.body) : hasTextInExpression(init.body);
}
} else if (t.isFunctionDeclaration(binding.path.node)) {
// Handle function declarations
return hasTextInReturnStatement(binding.path.node.body);
}
return false;
});
}
/**
* Check if a block statement contains a return statement with a Text element
*/
function hasTextInReturnStatement(blockStatement: t.BlockStatement): boolean {
for (const statement of blockStatement.body) {
if (t.isReturnStatement(statement) && statement.argument && hasTextInExpression(statement.argument)) {
return true;
}
}
return false;
}
/**
* Check if an expression contains a Text element
*/
function hasTextInExpression(expression: t.Expression): boolean {
// If directly returning a JSX element
if (t.isJSXElement(expression)) {
// Check if it's a Text element
if (t.isJSXIdentifier(expression.openingElement.name, { name: 'Text' })) {
return true;
}
// Check if any children are Text elements
for (const child of expression.children) {
if (t.isJSXElement(child) && t.isJSXIdentifier(child.openingElement.name, { name: 'Text' })) {
return true;
}
}
}
return false;
}

However, we don't have a way of checking that across file boundaries yet. For example, this will currently optimize the View component, even though it shouldn't:

// CustomComponent.tsx

import { Text } from 'react-native';

export function CustomComponent() {
  return (
    <Text>{children}</Text>
  );
}

// SomeScreen.tsx

import { View } from 'react-native';
import { CustomComponent } from './CustomComponent';

export default function CustomComponent() {
  return (
    <CustomComponent>
      <View /> {/* This should not be optimized! */}
    </CustomComponent>
  );
}

Discussion

This is likely out of scope for a Babel plugin. Babel only looks at one file at a time and – as far as I know – does not have the capabilities for a full-fledged module graph. The only two possibilities would therefore be growing out of the Babel plugin or disabling optimizations of Views with ancestors that can't be resolved from the current file, and putting it behind a configuration flag like __dangerouslyOptimizeViewWithCrossFileAncestor.

Workaround

To prevent issues, manually exclude either the file or the line from the optimization. For example:

// SomeScreen.tsx

import { CustomComponent } from './CustomComponent';

export default function CustomComponent() {
  return (
    <CustomComponent>
      {/* @boost-ignore */}
      <View />
    </CustomComponent>
  );
}

Alternatively, disable the whole View optimizer (as seen in the benchmarks, the Text optimizer is far more important when it comes to improving performance):

// babel.config.js

module.exports = {
  plugins: [
    [
      'react-native-boost/plugin',
      {
        optimizers: {
          view: false,
        },
      },
    ],
  ],
};

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is neededpinnedWill not be marked as stale

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions