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.
- Boolean props:
isDisabled
- String props:
className="hello"
- Expression props:
counter={5+4}
Inserting a boolean prop
Insert a new boolean prop isDisabled
.
Transform:
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();
}
Input:
import React from 'react';
const Button = (props) => {
return <button>Submit</button>;
};
Output:
import React from 'react';
const Button = props => {
- return <button>Submit</button>;
+ return <button isDisabled>Submit</button>;
};
Inserting a string prop
Insert a new string prop className
.
Transform:
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();
}
Input:
import React from 'react';
const Button = (props) => {
return <button>Submit</button>;
};
Output:
import React from 'react';
const Button = props => {
- return <button>Submit</button>;
+ return <button className="helloWorld">Submit</button>;
};
Removing props
Transform:
export default function transformer(file, { jscodeshift: j }, options) {
const source = j(file.source);
source
.find(j.JSXElement)
.filter((path) => path.value.openingElement.name.name === 'button') // Find all button jsx elements
.find(j.JSXAttribute) // Find all attributes (props) on the button
.filter((path) => path.node.name.name === 'onClick') // Filter to only props called onClick
.remove(); // Remove everything that matched
return source.toSource();
}
Input:
import React from 'react';
const Button = (props) => {
return (
<button className="button" onClick={() => console.log('Hello, World!')}>
Submit
</button>
);
};
Output:
import React from 'react';
const Button = props => {
- return <button className="button" onClick={() => console.log('Hello, World!')}>Submit</button>;
+ return <button className="button">Submit</button>;
};
Updating props
Transform:
export default function transformer(file, { jscodeshift: j }, options) {
const source = j(file.source);
source
.find(j.JSXElement)
.filter((path) => path.value.openingElement.name.name === 'button') // Find all button jsx elements
.find(j.JSXAttribute) // Find all attributes (props) on the button
.filter((attribute) => attribute.node.name.name === 'className') // Filter to only props called className
.find(j.Literal) // Narrow further to the literal value (note: This may not always be a Literal)
.forEach((literal) => {
literal.node.value = literal.node.value + ' button-primary';
}); // Mutate the value using the current value
return source.toSource();
}
Input:
import React from 'react';
const Button = (props) => {
return <button className="button">Submit</button>;
};
Output:
import React from 'react';
const Button = props => {
- return <button className="button">Submit</button>;
+ return <button className="button button-primary">Submit</button>;
};
Renaming props
Transform:
export default function transformer(file, { jscodeshift: j }, options) {
const source = j(file.source);
source
.find(j.JSXElement)
.filter((path) => path.value.openingElement.name.name === 'button') // Find all button jsx elements
.find(j.JSXAttribute) // Find all attributes (props) on the button
.filter((attribute) => attribute.node.name.name === 'data-foo') // Filter to only props called data-foo
.forEach((attribute) =>
j(attribute).replaceWith(
j.jsxAttribute(j.jsxIdentifier('data-bar'), attribute.node.value)
)
); // Reconstruct the attribute, replacing the name and keeping the value
return source.toSource();
}
Input:
import React from 'react';
const Button = (props) => {
return <button data-foo="bar">Submit</button>;
};
Output:
import React from 'react';
const Button = props => {
- return <button data-foo="bar">Submit</button>;
+ return <button data-bar="bar">Submit</button>;
};
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).
Transform:
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();
}
Input:
import React from 'react';
const Button = () => {
return <button className="button">Submit</button>;
};
Output:
import React from 'react';
const Button = () => {
+ const props = { className: 'button' };
- return <button className="button">Submit</button>;
+ return <button {...props}>Submit</button>;
};
JSX
Wrapping components
Wrapping react components with react components is a fairly common operation.
Simply follow this fairly simple set of steps:
- Find the component you want to wrap
- Create a new component and pass the component to be wrapped in as a child node
- Replace the original component with a wrapped version of itself
Transform:
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();
}
Input:
import { Avatar, Tooltip } from 'component-lib';
const App = () => {
return <Avatar name="foo" />;
};
Output:
import {Avatar, Tooltip } from 'component-lib';
const App = () => {
return (
+ <Tooltip content="foo">
<Avatar name="foo" />
+ </Tooltip>
);
}
Inserting children nodes
Transform:
export default function transformer(file, { jscodeshift: j }, options) {
const source = j(file.source);
source
.find(j.JSXElement) // Find all jsx elements
.filter((path) => path.node.openingElement.name.name === 'ul') // filter to an array of only ul elements
.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();
}
Input:
import React from 'react';
const Button = () => {
return (
<ul>
<li>Bulbasaur</li>
<li>Ivysaur</li>
</ul>
);
};
Output:
import React from 'react';
const Button = () => {
return (
<ul>
<li>Bulbasaur</li>
<li>Ivysaur</li>
+ <li>Venusaur</li>
</ul>
);
};
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.
Transform:
import { getJSXAttributes } from '@hypermod/utils';
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();
}
Input:
import Avatar from '@component-lib/avatar';
const App = () => {
return <Avatar name="Dan" component={CustomAvatar} />;
};
Output:
import Avatar from '@component-lib/avatar';
const App = () => {
return (
- <Avatar name="Dan" component={CustomAvatar} />
+ <Avatar name="Dan">{props => <CustomAvatar {...props} />}</Avatar>
+ );
}