Using ast-grep to restructure a React prop
In First use of ast-grep, I wrote about how I made use of ast-grep to rename a variable throughout a codebase that I couldn’t do with just ripgrep and sd. In that case, I reached out to ast-grep because the text I wanted to change was a variable only, and ast-grep gave me the ability to target only variables.
This time, I wanted to restructure a prop passed to a specific component in our React codebase. Below is a simplified example of this.
const ParentComponent = () => {
return (
<ChildComponent
// Convert from this prop...
before={{
target: "foo",
}}
// ...to this prop
target="foo"
/>
);
};
Again, I thought that ast-grep would be perfect for this. Because it has an awareness of the abstract syntax tree (AST), we can use it to extract the exact string value "foo" from within the object value of the before prop and convert it to the new target prop value.
I spent quite a bit more time figuring out how to make the exact change compared to what I did before, because this change was significantly more complex. I needed to match an entire branch of the syntax tree, yet only extract a sub-section of that branch. The rule file I ended up with is as follows.1
id: restructure-prop
language: tsx
rule:
# Top-level rule should match the entire AST node to change
all:
# `patterns` create variables to use when rewriting the tree
- pattern: $PROP # this is unused in this case
- kind: jsx_attribute
- has:
all:
- kind: property_identifier
- regex: "^before$"
- has:
all:
- kind: jsx_expression
- has:
all:
- kind: object
- has:
all:
- kind: pair
- has:
all:
# pattern deep in the tree is possible
- pattern: $TARGET_STR
- kind: string
- inside:
all:
- kind: jsx_self_closing_element
has:
all:
- kind: identifier
- regex: "^ChildComponent$"
# fix changes the top-level matched node
fix: target=$TARGET_STR
Understanding the rule
The top-level rule has several requirements to match. If we go down the list in order, the top-level node is a jsx_attribute node which has a property_identifier node with the value matching only the string “before”. This means the top-level node is a prop named “before” that is being passed to a component.
The next requirement is a long list of has and all rules that ends with the pattern $TARGET_STR and kind string. This deep nesting is to precisely match the target property’s value in the before prop to the $TARGET_STR variable. After using this rule, I learnt that we can simplify the rule by using the stopBy rule, so that we don’t have to write every single level of nesting. Also, I learnt that we don’t need the all rule at every level too. At the end of the article, I will share a much simpler rule that also works for this specific use case.
The last requirement makes sure that the prop we’re targeting is a descendent of a component called ChildComponent, and only self-closing components are matched. So <ChildComponent /> is matched, while <ChildComponent></ChildComponent> is not.
Because the matched node is the jsx_attribute node, i.e. the entire prop and its values, the fix applies to the entire prop, so we can change it to exactly what we need. As we used the $TARGET_STR pattern to match out the value of the target property that we need, we can use it in the fix section to ensure that we get the same value applied to the new before prop no matter what that value is.
Improvements
id: restructure-prop
language: tsx
rule:
kind: jsx_attribute
all:
- has:
kind: property_identifier
regex: "^before$"
- has:
kind: string
stopBy: end
pattern: $TARGET_STR
- inside:
kind: jsx_self_closing_element
has:
kind: identifier
regex: "^ChildComponent$"
fix: target=$TARGET_STR
As mentioned in the previous section, there are ways to simplify the initial rule that I used2. In creating another rule, I came across the stopBy rule while searching for ways to not need to write every single level of nesting, because I wanted to match something that had a much deeper nesting than the rule in this article. Using stopBy, we can greatly simply the nested chain to just a single level, because this rule will stop at the end which is what we need: the string value.