React & JSX

Learn how to use codemods to modify your React and JSX code. This guide covers inserting or removing props, wrapping components, and managing render props.


Codemods are an ideal solution for making significant changes to React and JSX code. The structure of JSX makes it easy for static analysis tools to understand and analyze, making it a popular choice for authors of React libraries, such as Design Systems. By providing codemods to users, they can streamline adoption, ensuring that component usage remains current and secure as the components evolve.

This guide provides information on transforming React and JSX code, including inserting or removing props, wrapping components, and managing render props. With the help of these techniques, you can make changes to your codebase with ease and confidence.

Props

Props in jscodeshift are represented by the j.JSXAttribute node type, these can be as a simple array on JSX elements (j.JSXElement).

It's important to remember props can be represented an a few different forms, their AST counterparts will change to match.

Inserting a boolean prop

Insert a new boolean prop isDisabled.

export default function transformer(file, { jscodeshift: j }, options) {
  const source = j(file.source);

  source
    .find(j.JSXElement)
    // Find all components called button
    .filter((path) => path.value.openingElement.name.name === 'button')
    .forEach((element) => {
      element.node.openingElement.attributes = [
        ...element.node.openingElement.attributes,
        // build and insert our new prop
        j.jsxAttribute(j.jsxIdentifier('isDisabled'))
      ];
    });

  return source.toSource();
}

import React from 'react';

const Button = (props) => {
  return <button>Submit</button>;
};

loading
Read-only

Inserting a string prop

Insert a new string prop className.

export default function transformer(file, { jscodeshift: j }, options) {
  const source = j(file.source);

  source
    .find(j.JSXElement)
    // Find all components called button
    .filter((path) => path.value.openingElement.name.name === 'button')
    .forEach((element) => {
      element.node.openingElement.attributes = [
        ...element.node.openingElement.attributes,
        // build and insert our new prop
        j.jsxAttribute(
          j.jsxIdentifier('className'),
          j.stringLiteral('helloWorld')
        )
      ];
    });

  return source.toSource();
}
import React from 'react';

const Button = (props) => {
  return <button>Submit</button>;
};

loading
Read-only

Removing props

export default function transformer(file, { jscodeshift: j }, options) {
  const source = j(file.source);

  source
    .find(j.JSXElement)
     // Find all button jsx elements
    .filter((path) => path.value.openingElement.name.name === 'button')
    // Find all attributes (props) on the button
    .find(j.JSXAttribute)
    // Filter to only props called onClick
    .filter((path) => path.node.name.name === 'onClick')
    // Remove everything that matched
    .remove();

  return source.toSource();
}
import React from 'react';

const Button = (props) => (
  <button className="button" onClick={() => console.log('Hello, World!')}>
    Submit
  </button>
);

loading
Read-only

Updating props

export default function transformer(file, { jscodeshift: j }, options) {
  const source = j(file.source);

  source
    .find(j.JSXElement)
    // Find all button jsx elements
    .filter((path) => path.value.openingElement.name.name === 'button')
    // Find all attributes (props) on the button
    .find(j.JSXAttribute)
    // Filter to only props called className
    .filter((attribute) => attribute.node.name.name === 'className')
     // Narrow further to the literal value (note: This may not always be a Literal)
    .find(j.Literal)
    .forEach((literal) => {
      // Mutate the value using the current value
      literal.node.value = literal.node.value + ' button-primary';
    });

  return source.toSource();
}
import React from 'react';

const Button = (props) => {
  return <button className="button">Submit</button>;
};

loading
Read-only

Renaming props

export default function transformer(file, { jscodeshift: j }, options) {
  const source = j(file.source);

  source
    .find(j.JSXElement)
    // Find all button jsx elements
    .filter((path) => path.value.openingElement.name.name === 'button')
    // Find all attributes (props) on the button
    .find(j.JSXAttribute)
    // Filter to only props called data-foo
    .filter((attribute) => attribute.node.name.name === 'data-foo')
    .forEach((attribute) =>
    // Reconstruct the attribute, replacing the name and keeping the value
      j(attribute).replaceWith(
        j.jsxAttribute(j.jsxIdentifier('data-bar'), attribute.node.value)
      )
    );

  return source.toSource();
}

import React from 'react';

const Button = (props) => {
  return <button data-foo="bar">Submit</button>;
};

loading
Read-only

Spread props

Spread props (aka spread attributes), allow you to pass an entire object into a jsx element as props.

They are represented by the following notation:

function App() {
  const props = { firstName: 'Ben', lastName: 'Hector' };
  return <Greeting {...props} />;
}

As an AST, these are represented by the j.JSXSpreadAttribute node, which accepts an j.Identifier (name) and j.ObjectExpression (props).

export default function transformer(file, { jscodeshift: j }, options) {
  const source = j(file.source);
  const props = [];

  source
    // Find all jsx elements with the name "button"
    .find(j.JSXElement)
    .filter((path) => path.node.openingElement.name.name === 'button')
    // Collect all of their props
    .forEach((path) => props.push(...path.node.openingElement.attributes))
    // Now get all of the jsx attributes (props)...
    .find(j.JSXAttribute)
    // And replace them with a spread attribute called "props" for example `{...props}`
    .forEach((path) =>
      path.replace(j.jsxSpreadAttribute(j.identifier('props')))
    );

  // Create a new constant variable named props.
  const variableDeclaration = j.variableDeclaration('const', [
    j.variableDeclarator(
      j.identifier('props'),
      // the variable will be assigned an object containing all of the props from button
      j.objectExpression(
        props.map((prop) =>
          j.objectProperty(
            j.identifier(prop.name.name),
            j.stringLiteral(prop.value.value)
          )
        )
      )
    )
  ]);

  // Finally, we find the arrow function expression
  source
    .find(j.ArrowFunctionExpression)
    // We then retrieve its body, which is the "block scope" of the component
    .get('body')
    // Since elements in a block are an array, we need to insert our new variable using unshift because we want it to be first
    .value.body.unshift(variableDeclaration);

  return source.toSource();
}
import React from 'react';

const Button = () => {
  return <button className="button">Submit</button>;
};
loading
Read-only

JSX

Wrapping components

Wrapping react components with react components is a fairly common operation.

Simply follow this fairly simple set of steps:

  1. Find the component you want to wrap
  2. Create a new component and pass the component to be wrapped in as a child node
  3. Replace the original component with a wrapped version of itself
export default function transformer(file, { jscodeshift: j }, options) {
  const source = j(file.source);

  // Find all components named "Avatar"
  source.findJSXElements('Avatar').forEach((element) => {
    // Create a new JSXElement called "Tooltip" and use the original Avatar component as children
    const wrappedAvatar = j.jsxElement(
      j.jsxOpeningElement(j.jsxIdentifier('Tooltip'), [
        // Create a prop on the tooltip so it works as expected
        j.jsxAttribute(
          j.jsxIdentifier('content'),
          j.stringLiteral('Hello, there!')
        )
      ]),
      j.jsxClosingElement(j.jsxIdentifier('Tooltip')),
      [element.value] // Pass in the original component as children
    );

    j(element).replaceWith(wrappedAvatar);
  });

  return source.toSource();
}
import { Avatar, Tooltip } from 'component-lib';

const App = () => {
  return <Avatar name="foo" />;
};
loading
Read-only

Inserting children nodes

Inserting children JSX nodes is not unlike insertion of props, but instead of adding to the attributes array, we add to the children array.

export default function transformer(file, { jscodeshift: j }, options) {
  const source = j(file.source);

  source
    // Find all jsx elements
    .find(j.JSXElement)
    // filter to an array of only ul elements
    .filter((path) => path.node.openingElement.name.name === 'ul')
    .forEach((path) =>
      // Replace each ul element with a modified version of itself
      path.replace(
        j.jsxElement(path.node.openingElement, path.node.closingElement, [
          ...path.node.children, // Copy existing children
          // Create a new li element containing our new entry
          j.jsxElement(
            j.jsxOpeningElement(j.jsxIdentifier('li')),
            j.jsxClosingElement(j.jsxIdentifier('li')),
            [j.stringLiteral('Venusaur')]
          ),
          j.jsxText('\n') // Add this to tidy up the formatting
        ])
      )
    );

  return source.toSource();
}
import React from 'react';

const Button = () => {
  return (
    <ul>
      <li>Bulbasaur</li>
      <li>Ivysaur</li>
    </ul>
  );
};

loading
Read-only

Render props

Moving between different types of React composition strategies, like for example, from component props to render props is could be something you want to do between major versions. This might seem difficult on the surface, but think about it like every other codemod. First we need to find the component, replace it with a modified copy of itself and finally insert a function as children.

export default function transformer(file, { jscodeshift: j }, options) {
  const source = j(file.source);

  source.findJSXElements('Avatar').forEach((element) => {
    // Find props all JSXAttributes with a prop called "component"
    // (Using the getJSXAttributeByName util here for simplicity)
    const componentProp = getJSXAttributes(j, element, 'component').get();
    // Grabs the name of the component passed into the "component" prop
    const componentName = j(componentProp)
      .find(j.JSXExpressionContainer)
      .find(j.Expression)
      .get().value.name;

    // Remove it since it's no longer required on the wrapping component
    j(componentProp).remove();

    // Create a new child component based on the component prop and spread props onto it
    const customComponent = j.jsxElement(
      j.jsxOpeningElement(
        j.jsxIdentifier(componentName),
        [j.jsxSpreadAttribute(j.identifier('props'))],
        true
      )
    );

    /**
     * Here's where it gets interesting.
     * We create a render prop function and pass in `customComponent` as the return value
     */
    const childrenExpression = j.jsxExpressionContainer(
      j.arrowFunctionExpression([j.identifier('props')], customComponent)
    );

    /**
     * Then finally, we replace our original component with the following.
     * Taking properties from the original component and combining them with our new render prop function
     */
    j(element).replaceWith(
      j.jsxElement(
        j.jsxOpeningElement(
          element.value.openingElement.name,
          element.value.openingElement.attributes,
          false
        ),
        j.jsxClosingElement(element.value.openingElement.name),
        [childrenExpression]
      )
    );
  });

  return source.toSource();
}

// Utility function to find JSXAttributes by name
function getJSXAttributes(j, element, attributeName) {
  return j(element)
    .find(j.JSXOpeningElement)
    .find(j.JSXAttribute)
    .filter(attribute => {
      const matches = j(attribute)
        .find(j.JSXIdentifier)
        .filter(identifier => identifier.value.name === attributeName);
      return Boolean(matches.length);
    });
}

import Avatar from '@component-lib/avatar';

const App = () => {
  return <Avatar name="Dan" component={CustomAvatar} />;
};

loading
Read-only