🦾

ast-grepでReact 19に移行する

2024/05/03に公開

はじめに

こんにちは、ast-grepの作者ヘリントンです。

Reactバージョン19のリリースに伴い、新機能と改善が追加されました。

画像の説明

しかし、この新バージョンへのアップグレードには、ソースコードの一部を修正する必要があります。特に大規模なコードベースでは、このプロセスはかなり手間がかかり、繰り返し行う必要があります。

本記事では、ast-grepというツールの使用方法を説明します。このツールは、コードベース内でパターンを見つけて置き換えることを目的として設計されており、React 19への移行を容易にします。

以下の3つの主要なcodemodsに焦点を当てます。

  • <Context>をプロバイダとして使用する
  • 暗黙のrefコールバックリターンを削除する
  • refをpropsとして使用し、forwardRefを削除する

前提条件: ast-grepのセットアップ

まず、ast-grepをセットアップする必要があります。これはnpmを通じて行うことができます。

npm install -g @ast-grep/cli

インストールが完了したら、以下のコマンドを実行してast-grepが正しくセットアップされていることを確認します。

ast-grep --version

<Context>をプロバイダとして使用する

最も簡単な修正から始めましょう:<Context>をプロバイダとして使用します。

React 19では、<Context.Provider>の代わりに<Context>をプロバイダとして使用します:

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

  );
}

ast-grepを使用すると、パターン$CONTEXT.Providerを見つけて$CONTEXTに置き換えることができます。
ただし、パターン$CONTEXT.Providerは複数の場所に現れる可能性があるため、JSXの開始要素と終了要素の中で特定のパターンを探す必要があります。

ast-grep playgroundを利用して、JSXの開始要素と終了要素の正確な名前を見つけることができます。

画像の説明

その後、insideanyを使用して、パターンがJSXの開始要素と終了要素の中に存在することを特定できます。

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

プレイグラウンドの例.

画像の説明

以下のコマンドを使用して、YAMLファイルからルールを起動できます:

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

暗黙のrefコールバックリターンを削除する

次の例は、暗黙のrefの戻り値を削除するものです。

React 19では、refコールバックからクリーンアップ関数を返すことができるようになりました。

refクリーンアップ関数の導入により、refコールバックから他のものを返すことはTypeScriptによって拒否されるようになりました。修正方法は通常、暗黙の戻り値の使用をやめることです。例えば、次のようにします:

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

このパターンをどのように見つけることができるでしょうか?<div ref={$A => $B}/>というパターンを見つける必要があります。ここで $B はステートメントブロック(statement block)であってはならない。

まず、ast-grepはpattern objectを使用してjsx_attributeを見つけることができます。これはJSX要素の属性のAST種類です。

ast-grepのプレイグラウンドを使って種類名を見つけることができます。

画像説明

次に、<div ref={$A => $B}/>というパターンを見つける必要があります。このパターンは、コールバック関数を持つref属性を持つJSX要素を探していることを意味します。

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

最後に、$Bがステートメントブロックでないことを確認する必要があります。constraintsフィールドを使用してこの条件を指定することができます。

constraints:
  B:
    not: {kind: statement_block}

これらすべてを組み合わせると、以下のルールが得られます:

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: ref={$A => {$B}}

Plagyground Screenshot

プレイグラウンドの例

forwardRefの削除

最も複雑な変更、すなわちforwardRefの削除について見てみましょう。今回の例では、アロー関数やTypeScriptが関与しない最も単純なケースに焦点を当てます。

例のコードは以下のようになります:

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

まずは簡単なところから始めましょう。forwardRef($FUNC)というパターンを見つけることができます。
しかし、$FUNCは関数の引数をキャプチャしません。関数の引数をキャプチャし、それらを修正で書き直す必要があります。

以下のパターンルールは関数の引数をキャプチャします。

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

次に、関数の引数を書き直す必要があります。最も簡単な方法は、$PROPSをオブジェクトの分割代入として書き直し、refをオブジェクトの分割代入に追加することです。

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

プレイグラウンドの例

Image description

より複雑なケースの対応

上記のルールは、単一の識別子引数をうまく処理します。しかし、Reactのコンポーネントでよく使われるオブジェクトの分割代入のような、より複雑なケースには対応していません。

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

より複雑なケースを処理するためには、rewritersを使用することができます。基本的な考え方は、書き換えをオブジェクトの分割代入と識別子の2つのシナリオに分けることです。

objectリライターはオブジェクトの分割代入パターンをキャプチャし、内部の内容を抽出します。

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

例えば、上記のリライターはfunction MyInput({value}, ref)の中の{value}をキャプチャし、value$$$ARGSとして抽出します。

identifierリライターは識別子パターンをキャプチャし、それをスプレッドします。

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

例えば、上記のリライターはpropsをキャプチャし、...propsとしてスプレッドします。

最後に、rewritersフィールドを使用して上記の2つのルールを登録します。

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

そして、transformフィールドを使用して引数を書き換え、それらを修正に使用します。

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

これらすべてをまとめると、最終的なルールが得られます:

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
  }

プレイグラウンドの例

Image description

結論

Codemodは、コードの変更を自動化する強力なパラダイムです。本記事では、ast-grepを使用してReact 19への移行方法を示しました。

<Context>をプロバイダとして使用する、implicit-ref-callback-returnを削除する、forwardRefを削除するという、3つの一般的な変更について説明しました。

ここでの例は教育目的であり、codemod.comを使用してこれらの変更をコードベースで自動化することを奨励します。codemod.comには、これらの変更やより微妙なエッジケースを処理するためのキュレーションされたルールがあります。

例を適応させ、ast-grepの力を使ってさらに多くのコードの変更を自動化することを探求することができます。

ast-grepはまた、codemod.comでも利用可能です。

Happy Coding!

Discussion