Migrate to React 19 with ast-grep

Migrate to React 19 with ast-grep

Introduction

The release of React version 19 has been accompanied with a set of new features and enhancements.

However, upgrading to this new version requires certain modifications in the source code. This process can be quite taxing and repetitive, particularly for large codebases.

This article illustrates the usage of ast-grep, a tool designed to locate and substitute patterns in your codebase, towards easing your migration to React 19.

We will focus on three major codemods:

Use <Context> as a provider
Remove implicit ref callback return
Use ref as props and remove forwardRef

Prerequisite: Setting up ast-grep

Initially, you need to set up ast-grep. This can be carried out via npm:

npm install -g @ast-grep/cli

Once installed, the proper set up of ast-grep can be confirmed by running the command below:

ast-grep –version

Use <Context> as a provider

Let’s kick off with the simplest modification: use as a provider.

React 19 uses <Context> as a provider instead of <Context.Provider>:

function App() {
const [theme, setTheme] = useState(‘light’);
// …
return (
– <UseTheme.Provider value={theme}>
+ <UseTheme value={theme}>
<Page />
– </UseTheme.Provider>
+ </UseTheme>

);
}

With ast-grep, the pattern $CONTEXT.Provider can be found and replaced with $CONTEXT.
However, the pattern $CONTEXT.Provider could appear in multiple locations, requiring us to specifically look for it within a JSX opening and closing element.

We can utilize the ast-grep playground to find the correct names for JSX opening elements and JSX closing elements.

Afterwards, using inside and any, we can pinpoint that the pattern should exist within a JSX opening element and a JSX closing element.

id: use-context-as-provider
language: javascript
rule:
pattern: $CONTEXT.Provider
inside:
any:
kind: jsx_opening_element
kind: jsx_closing_element
fix: $CONTEXT

Example playground link.

You can launch the rule from a YAML file using the command below:

ast-grep scan -r use-context-as-provider.yml

Remove implicit-ref-callback-return

Our next example is to remove implicit ref’s return.

React 19 now supports returning a cleanup function from ref callbacks.

Due to the introduction of ref cleanup functions, returning anything else from a ref callback will now be rejected by TypeScript. The fix is usually to stop using implicit returns, for example:

– <div ref={current => (instance = current)} />
+ <div ref={current => {instance = current}} />

How can we find this pattern? We should find the pattern <div ref={$A => $B}/> where $B is not a statement block.

First, ast-grep can use pattern object to find jsx_attribute, which is the AST kind for the attribute in a JSX element.

You can use ast-grep’s playground to find out the kind name.

Next, we need to find the pattern <div ref={$A => $B}/>. The pattern means that we are looking for a JSX element with a ref attribute that has a callback function.

pattern:
context: <div ref={$A => $B}/>
selector: jsx_attribute

Finally, we need to check if $B is not a statement block. We can use the constraints field to specify this condition.

constraints:
B:
not: {kind: statement_block}

Combining all these pieces, we get the following rule:

id: remove-implicit-ref-callback-return
language: javascript
rule:
pattern:
context: <div ref={$A => $B}/>
selector: jsx_attribute
constraints:
B:
not: {kind: statement_block}
fix: $A => {$B}

Example playground link

Remove forwardRef

Let’s explore our most complex change: Remove forwardRef. In our example, we will only focus on the simplest case where no arrow function nor TypeScript is involved.

The example code is as follows:

const MyInput = forwardRef(function MyInput(props, ref) {
return <input {props} ref={ref} />;
});

Let’s first start simple. We can find the pattern forwardRef($FUNC).
However, the $FUNC does not capture the arguments of the function. We need to capture the arguments of the function and rewrite them in the fix.

This pattern rule captures the function arguments.

rule:
pattern: forwardRef(function $M($PROPS, $REF) { $$$BODY })

Next, we need to rewrite the arguments of the function. The easiest way is to rewrite $PROPS as an object destructuring and to add ref into the object destructuring.

rule:
pattern: forwardRef(function $M($PROPS, $REF) { $$$BODY })
fix: |-
function $M({ref: $REF, …$PROPS}) {
$$$BODY
}

Example playground link

Handle more complex cases

The above rule handles single identifier arguments gracefully. But it does not work for more complex cases like object destructuring, which is popular in React components.

const MyInput = forwardRef(function MyInput({value}, ref) {
return <input value={value} ref={ref} />;
});

We can use rewriters to handle more complex cases. The basic idea is that we can break down the rewriting into two scenarios: object destructuring and identifiers.

The object rewriter captures the object destructuring pattern and extract the inner content.

id: object
rule:
pattern:
context: ({ $$$ARGS }) => {}
selector: object_pattern
fix: $$$ARGS

For example, the rewriter above will capture the {value} inside function MyInput({value}, ref) and extract value as $$$ARGS.

The identifier rewriter captures the identifier pattern and spreads it.

id: identifier
rule: { pattern: $P }
fix: …$P

For example, the rewriter above will capture props and spread it as …props.

Finally, we can use the rewriters field to register the two rules above.

rewriters:
id: object
rule:
pattern:
context: ({ $$$ARGS }) => {}
selector: object_pattern
fix: $$$ARGS
id: identifier
rule: { pattern: $P }
fix: …$P

And we then can use them in the transform field to rewrite the arguments and use them in the fix.

transform:
NEW_ARG:
rewrite:
rewriters: [object, identifier]
source: $PROPS
fix: |-
function $M({ref: $REF, $NEW_ARG}) {
$$$BODY
}

Putting all these together, we get the final rule:

id: remove-forward-ref
language: javascript
rule:
pattern: forwardRef(function $M($PROPS, $REF) { $$$BODY })
rewriters:
id: object
rule:
pattern:
context: ({ $$$ARGS }) => {}
selector: object_pattern
fix: $$$ARGS
id: identifier
rule: { pattern: $P }
fix: …$P
transform:
NEW_ARG:
rewrite:
rewriters: [object, identifier]
source: $PROPS
fix: |-
function $M({ref: $REF, $NEW_ARG}) {
$$$BODY
}

Example playground link

Conclusion

Codemod is a powerful paradigm to automate code changes. In this article, we have shown how to use ast-grep to migrate to React 19.

We have covered three common changes: use as a provider, remove implicit-ref-callback-return, and remove forwardRef.

The examples here are for educational purposes and you are encouraged to use codemod.com to automate these changes in your codebase. codemod.com has curated rules that take care of these changes and more subtle edge cases.

You can adapt the example and explore ast-grep’s power to automate more code changes.

ast-grep is also available on codemod.com.

Happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *