ast-grepでReact 19に移行する
はじめに
こんにちは、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の開始要素と終了要素の正確な名前を見つけることができます。
その後、inside
とany
を使用して、パターンが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}}
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
}
より複雑なケースの対応
上記のルールは、単一の識別子引数をうまく処理します。しかし、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
}
結論
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