DEV Community

Cover image for Migrate to React 19 with ast-grep
Herrington Darkholme
Herrington Darkholme

Posted on

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.

Image description

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
Enter fullscreen mode Exit fullscreen mode

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

ast-grep --version
Enter fullscreen mode Exit fullscreen mode

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>

  );
}
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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
Enter fullscreen mode Exit fullscreen mode

Example playground link.

Image description

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

ast-grep scan -r use-context-as-provider.yml
Enter fullscreen mode Exit fullscreen mode

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}} />
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

Image description

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} />;
});
Enter fullscreen mode Exit fullscreen mode

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 })
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

Example playground link

Image description

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} />;
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

Example playground link

Image description

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!

Top comments (0)