EY-Office ブログ

古いReactアプリをClaudeCodeにアップデートしてもらいました。動作は完璧でもUIは今イチかな

だいぶ前に書いたWebApchives iOSアプリで収集した情報をWebで表示するReactアプリを作ったのアプリは現在でも使っていますが少し不満が出てきたので改造したくなりました。

そもそも、このアプリは気になったWeb上の記事を保存するアプリです。保存されたタイトル、URL、コンテンツのPDFはFirebaseに格納されています。 → 詳しくはココ
このアプリはMacでは動かないので表示のみできるReactアプリを作りました。ただし、最近は保存されたコンテンツが増え、以下の不満を感じています。

  • 最初に(PDF以外の)全データを読み込むので、時間がかかる(3〜4秒)
  • コンテンツが増えて直ぐにコンテンツが見つけらいので、検索機能が欲しい

update-my-old-react-app

アプリが古い!

このReactアプリは2020年の正月休みに作ったもので、5年間アップデートしていませんでした。😅

  • Reactはバージョン16
  • 開発環境は、今は亡きCreate React App
  • Firebaseのバージョンは7。最新は12でAPIの仕様もかなり違います
  • スタイリングは、昔は流行った Material-UI v4styled-components
  • ・・・

これをアップデートしないと機能追加はできません、でも面倒ですよね・・・

Claude Codeでアップデート!

そうだ、面倒な仕事はClaude Codeにやらせよう❗

このディレクトリーのReactアプリをアップデートしてください。まずはアップデートの計画を作り
CLAUDE.md に書いてください。

- 開発環境をcreate-recat-appからViteに変更
- Reactを最新版にする
- Firebase APIを最新版にする
- スタイリングをMUIとstyled-componentsからTailwind CSSに変更する

出来たアップデートの計画のCLAUDE.mdはブログの末尾に置きました、アップデート計画はしっかりしているので早速実行ました。

Claude Codeのログ:

⏺ Reactアプリのアップデート計画を作成します。まず現在の状況を確認してから、CLAUDE.mdに詳細な計画を書きます。

⏺ Update Todos
  ⎿  ☐ 現在のプロジェクト構成を調査・分析する
     ☐ アップデート計画をCLAUDE.mdに作成する
     ☐ create-react-appからViteに移行する
     ☐ Reactを最新版にアップデートする
     ☐ Firebase APIを最新版にアップデートする
     ☐ MUI・styled-componentsからTailwind CSSに移行する

  ・・・・・・・・・

⏺ CLAUDE.mdにReactアプリのアップデート計画を作成しました。

  ・・・・・・・・・

  各フェーズは段階的に実装し、動作確認とテストを行います。特にFirebase APIの大幅変更とスタイリングの移行には注意が必要です。
  実装を開始しますか?どのフェーズから始めるか指示してください。

> 実装を開始してください

⏺ 実装を開始します。計画に従ってフェーズ1のVite移行から始めます。

  ・・・・・・・・・

⏺ アップデート完了

  Reactアプリのモダンなスタックへのアップデートが完了しました!

  実施した変更内容

  ✅ フェーズ1: Vite移行
  - create-react-appからViteに移行
  - 高速なHMR対応
  - vite.config.ts, tsconfig.node.jsonを設定

  ✅ フェーズ2: React 18更新
  - React 16.12.0 → 18.3.1
  - 新しいルートAPI (createRoot)を採用
  - モダンなReact機能を利用可能

  ✅ フェーズ3: Firebase v10移行
  - Firebase 7.6.1 → 10.13.0
  - 名前空間API → モジュラーSDKに移行
  - use-firebase-authを削除し、ネイティブFirebase Authを実装

  ✅ フェーズ4: Tailwind CSS移行
  - Material-UI v4 + styled-components → Tailwind CSS v4
  - モダンなユーティリティファーストのスタイリング
  - レスポンシブデザインを維持

  ✅ フェーズ5: 依存関係最適化
  - TypeScript 3.7.4 → 5.0.0
  - ES Modulesに対応 ("type": "module")
  - 古い依存関係を削除

  技術スタック(更新後)

  - ビルドツール: Vite 5.3.1
  - React: 18.3.1
  - Firebase: 10.13.0 (モジュラーSDK)
  - スタイリング: Tailwind CSS v4
  - TypeScript: 5.0.0
  - 開発: npm run dev
  - ビルド: npm run build
  - デプロイ: npm run deploy (distディレクトリから)

  アプリケーションは正常にビルドされ、モダンな開発環境で動作するようになりました。

> /exit
  ⎿  (no content)

Total cost:            $1.92
Total duration (API):  9m 33.7s
Total duration (wall): 36m 8.8s
Total code changes:    433 lines added, 183 lines removed
Usage by model:
    claude-3-5-haiku:  10.4k input, 619 output, 0 cache read, 0 cache write
       claude-sonnet:  162 input, 30.8k output, 3.9m cache read, 75.3k cache write

出来た新アプリを起動すると動きました 🎉
ログイン・ログアウト、一覧表示、コンテンツ表示、全てが動作しました。🎉

変換されたコード

出来たコードを読んでみると、良く出来ていました。

Firebase API
  const loadArticles = async () => {
    const db = firebase.firestore()
    setLoading(true)
    const querySnapshot = await db.collection("articles").orderBy("created_at", "desc").get()
    const articles = querySnapshot.docs.map(e => e.data() as Artcle)
    setArticles(articles)
    setLoading(false)
  }

   ↓ 変換

  const loadArticles = async () => {
    setLoading(true)
    const q = query(collection(db, "articles"), orderBy("created_at", "desc"))
    const querySnapshot = await getDocs(q)
    const articles = querySnapshot.docs.map(doc => doc.data() as Artcle)
    setArticles(articles)
    setLoading(false)
  }
use-firebase-auth

指示には書き忘れたのですが、Firebaseログイン・ログアウト処理を行ってくれるフックuse-firebase-authのアップデートは終了しているようでした。しかし、Claude Codeはこれに相当するFirebaseのAPIを使ったコードを追加してくれました。

const FirebaseAuth: React.FC = () => {
  const { user, signInWithProvider, signOut } = useFirebaseAuth()
  if (user) {
    return <Button onClick={() => signOut()}  color="inherit" style={{textTransform: 'none'}}>
      Logout </Button>
  } else {
    return <Button onClick={() => signInWithProvider(SIGNIN_PROVIDERS.GOOGLE)} color="inherit" style={{textTransform: 'none'}} >
      Login </Button>
  }
}

   ↓ 変換(一部省略)

const FirebaseAuth: React.FC = () => {
  const [user, setUser] = useState<User | null>(null)

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      setUser(user)
    })
    return () => unsubscribe()
  }, [])

  const handleSignIn = async () => {
    const provider = new GoogleAuthProvider()
    await signInWithPopup(auth, provider)
  }

  const handleSignOut = async () => {
    await signOut(auth)
  }

  if (user) {
    return (
      <button onClick={handleSignOut} className="px-4 py-2 bg-blue-700 hover:bg-blue-800 rounded text-white transition-colors">
        Logout </button>
    )
  } else {
    return (
      <button onClick={handleSignIn} className="px-4 py-2 bg-blue-700 hover:bg-blue-800 rounded text-white transition-colors">
        Login </button>
    )
  }
}

しかしUIは残念な結果に

しかし、画面(UI)が今イチ 😂

オリジナル

新アプリ

  1. アプリケーションバーが真っ白?
  2. ログイン・ログアウトボタンが素のボタン?
  3. 一覧のレイアウトがズレている

調べたところ、1、2はTailwind CSSの設定ファイルが間違が原因でした。ただし、設定ファイルを直しても3は直りませんでした。
やはり、画面情報無しでMaterial-UI・styled-componentsからTailwind CSSへの変換は難しそうですね。

変換されたコード

がんばって変換してくれてますね。

import {List, ListRowProps} from 'react-virtualized'

const ArctcleList: React.FC<ArctcleListProps> = ({artcles, listWidth, listHeight, clicked}) => {
  const [selectedIx, setSelectedIx] = useState(-1)
  const listRowHeight = 80

  const rowRenderer = ({ index,  key, style }: ListRowProps) => {
    const toYMD = (ts:firebase.firestore.Timestamp): string =>
      fecha.format(ts.toDate(), 'YYYY-MM-DD')
    const blobToImage = (blob: firebase.firestore.Blob): string =>
      blob ? "data:image/png;base64," + blob.toBase64() : ""

    const ArticleRow = styled.div`
      position: absolute; left: 0; top: ${style.top}px;
      width: ${listWidth - 20}px;  height: ${listRowHeight}px;
      border: solid 0.5px #888
      display: grid;
      grid-template-rows: 60px 20px;
      grid-template-columns: 72px 1fr;
      `
    const Thumb = styled.img`
      grid-row: 1 / 3;
      grid-column: 1 / 2;
      margin: ${(listRowHeight - 60) / 2}px 4px;
      height: 60px; max-width: 60px;
      `
    const Title = styled.div`
      grid-row: 1 / 2;
      grid-column: 2 / 3;
      margin: 6px 4px 0 0;
      height: 46px; overflow: hidden; font-size: 18px; line-height: 23px;
      font-weight: ${selectedIx === index ? 800 : 400};
      display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
      `

    const LinkUrl = styled.a`
      grid-row: 2 / 3;
      grid-column: 2 / 2;
      font-size: 13px; color: #666; text-align: left; font-family: Andale Mono;
      `

    const DateField = styled.div`
      grid-row: 2 / 3;
      grid-column: 3 / 3;
      margin-right: 4px;
      font-size: 13px; color: #666; text-align: right; font-family: Andale Mono;
      `

    return (
      <ArticleRow key={key} onClick={() => {clicked(artcles[index]); setSelectedIx(index)}} >
        <Thumb src={blobToImage(artcles[index].thumb)}/>
        <Title>{artcles[index].title}</Title>
        {artcles[index].url ? <LinkUrl href={artcles[index].url} target="_blank">Web</LinkUrl> : null}
        <DateField>{toYMD(artcles[index].created_at)}</DateField>
      </ArticleRow>
    )
  }

  return (
    <List
      rowCount={artcles.length}
      width={listWidth}
      height={listHeight}
      rowHeight={listRowHeight}
      rowRenderer={rowRenderer}
      overscanRowCount={3}
      style={{outline: "none"}} />
  )
}

  ↓ 変換

const ArctcleList: React.FC<ArctcleListProps> = ({artcles, listWidth, listHeight, clicked}) => {
  const [selectedIx, setSelectedIx] = useState(-1)
  const listRowHeight = 80

  const rowRenderer = ({ index,  key, style }: ListRowProps) => {
    const toYMD = (ts: any): string => {
      if (ts && ts.toDate) {
        return fecha.format(ts.toDate(), 'YYYY-MM-DD')
      }
      return ''
    }
    const blobToImage = (blob: any): string => {
      if (blob && blob.toBase64) {
        return "data:image/png;base64," + blob.toBase64()
      }
      return ""
    }

    const articleRowStyle: React.CSSProperties = {
      position: 'absolute',
      left: 0,
      top: style.top,
      width: listWidth - 20,
      height: listRowHeight
    }

    return (
      <div 
        key={key} 
        style={articleRowStyle}
        className={`border border-gray-400 grid grid-rows-2 grid-cols-[72px_1fr] cursor-pointer hover:bg-gray-50 ${
          selectedIx === index ? 'bg-blue-50' : ''
        }`}
        onClick={() => {clicked(artcles[index]); setSelectedIx(index)}}
      >
        <img 
          src={blobToImage(artcles[index].thumb)}
          className="row-span-2 col-span-1 h-15 max-w-15 m-1"
          style={{margin: `${(listRowHeight - 60) / 2}px 4px`, height: '60px', maxWidth: '60px'}}
        />
        <div 
          className={`row-start-1 col-start-2 mx-1 mt-1.5 h-11 overflow-hidden text-lg leading-6 ${
            selectedIx === index ? 'font-bold' : 'font-normal'
          } line-clamp-2`}
        >
          {artcles[index].title}
        </div>
        {artcles[index].url ? (
          <a 
            href={artcles[index].url} 
            target="_blank" 
            className="row-start-2 col-start-2 text-sm text-gray-600 text-left font-mono"
          >
            Web
          </a>
        ) : null}
        <div className="row-start-2 col-start-3 mr-1 text-sm text-gray-600 text-right font-mono">
          {toYMD(artcles[index].created_at)}
        </div>
      </div>
    )
  }

  return (
    <List
      rowCount={artcles.length}
      width={listWidth}
      height={listHeight}
      rowHeight={listRowHeight}
      rowRenderer={rowRenderer}
      overscanRowCount={3}
      style={{outline: "none"}} />
  )
}

その後は、どうしたか

実は私はコードを書くのが大好きなので、変換されたコードを利用しながら新規にアプリを作り直してしまいました。😃

上のコードは以下のようになりました。シンプルです。😃
他の部分も考え直して作り直したので、全体的にシンプルになりました。

function ArticleList({ articles, clicked }: { articles: Artcle[], clicked: (article: Artcle) => void }) {
  const blobToImage = (blob: firebase.firestore.Blob): string =>
    blob ? "data:image/png;base64," + blob.toBase64() : "";

  return (
    <div style={{ maxHeight: "calc(100vh - 64px)", overflowY: "auto" }}>
    {articles.map((article, index) => (
        <div key={index} onClick={() => {clicked(article)}}
          className="flex p-1 h-[80px] border-b border-x border-gray-400 hover:cursor-pointer">
          <img src={blobToImage(article.thumb)} alt={article.title} className="h-[60px] max-w-[60px]" />
          <div className="ml-3 pr-2">
            <div className="line-clamp-2 h-[52px] hover:underline">{article.title}</div>
            <div className="flex">
              {article.url &&
                <a href={article.url} className="text-sm font-mono text-gray-600 hover:underline flex-auto" target="_blank">Web</a>}
                <span className="flex-auto"></span>
              <span className="text-sm font-mono text-gray-600">{article.created_at.toDate().toLocaleDateString('sv-SE')}</span>
            </div>
          </div>
        </div>
      ))}
    </div>
  )
}

そして、全データ読み込み前の一覧表示や、検索機能の追加も行えました。

まとめ

Claude Codeを使って古いコードをアップデートするのは、とても効果があると判りました。今回はUIは今イチでしたが、画面情報を画像等で与えておけばもう少し良かったかも知れませんね。

長い間ほったらかしにしたコードのアップデートは乗り気にはなりませんよね。その部分はClaude Code(AI)に任せて、自分(人間)は面白味・創造性のある機能追加やリファクタリングを行うのは望ましい開発のスタイルかも知れませんね。😃

 
 


CLAUDE.md
# React Web Archives アップデート計画

## 現在の状況分析

### 現在の技術スタック
- **ビルドツール**: create-react-app (react-scripts 3.3.0)
- **React**: 16.12.0 (古い)
- **Firebase**: 7.6.1 (古い)
- **スタイリング**: 
  - Material-UI v4.8.1 (@material-ui/core)
  - styled-components 4.4.1
- **TypeScript**: 3.7.4 (古い)
- **その他の依存関係**:
  - react-virtualized
  - use-firebase-auth
  - fecha

### 主要な問題点
1. Node.js の古いOpenSSLサポートが必要 (`--openssl-legacy-provider`)
2. React 16.12.0 は非常に古く、セキュリティとパフォーマンスの問題
3. Firebase v7 は廃止予定で新機能がサポートされない
4. Material-UI v4 は Material-UI v5/MUI に移行済み
5. TypeScript 3.7.4 は最新機能がサポートされない

## アップデート計画

### フェーズ 1: ビルドツールの移行 (create-react-app → Vite)

#### 1.1 Viteの設定
- `vite.config.ts` の作成
- 環境変数の設定 (`.env` ファイル)
- TypeScript設定の調整
- public フォルダーの調整

#### 1.2 依存関係の更新
- `react-scripts` の削除
- Vite関連パッケージの追加:
  - `vite`
  - `@vitejs/plugin-react`
  - `@types/node` (最新版)

#### 1.3 スクリプトの更新
- `package.json` のscriptsをVite用に変更
- ビルド出力ディレクトリの調整 (`build``dist`)

### フェーズ 2: React の最新化

#### 2.1 React 18への移行
- React 18.x への更新
- React DOM の更新
- 新しいルートAPIの採用 (`createRoot`)

#### 2.2 型定義の更新
- `@types/react``@types/react-dom` の最新版への更新
- TypeScript 5.x への更新

### フェーズ 3: Firebase の最新化

#### 3.1 Firebase v9 モジュラーSDKへの移行
- Firebase 10.x への更新
- インポート方式の変更 (名前空間 → モジュラー)
- 認証ライブラリの変更検討

#### 3.2 Firebase設定の更新
- 新しいSDKの初期化方式
- Firestore、Storage、Authの新しいAPI使用

### フェーズ 4: スタイリングの移行 (MUI + styled-components → Tailwind CSS)

#### 4.1 Tailwind CSSの導入
- Tailwind CSS、PostCSS、Autoprefixerの追加
- `tailwind.config.js` の設定
- PostCSS設定

#### 4.2 コンポーネントの段階的移行
1. **AppBar/Toolbar** → Tailwind のヘッダーコンポーネント
2. **Grid システム** → Tailwind のFlexbox/Gridクラス
3. **Button** → Tailwind のボタンスタイル
4. **Typography** → Tailwind のテキストクラス
5. **styled-components** → Tailwind クラス

#### 4.3 レスポンシブデザインの維持
- 既存のGrid システム(xs=3, xs=9)をTailwind の `grid-cols-12` で再現
- モバイル対応の確認

### フェーズ 5: その他の依存関係の更新

#### 5.1 仮想化ライブラリの検討
- `react-virtualized``@tanstack/react-virtual` への移行検討
- パフォーマンステスト

#### 5.2 日付ライブラリの更新
- `fecha``date-fns` または `dayjs` への移行検討

## 実装順序

1. **Vite移行** (最優先 - ビルド環境の安定化)
2. **React 18更新** (互換性確保)
3. **Firebase v10移行** (APIの大幅変更のため慎重に)
4. **Tailwind CSS移行** (UI の段階的変更)
5. **その他依存関係の更新**

## リスク管理

### 高リスク項目
- Firebase APIの大幅変更による動作不良
- styled-components から Tailwind への移行でのレイアウト崩れ
- react-virtualized の代替ライブラリでのパフォーマンス劣化

### 対策
- 各フェーズでの動作確認
- バックアップブランチの作成
- 段階的なテストとロールバック計画

## 期待される効果

### パフォーマンス向上
- Viteによる高速なHMR
- React 18の新機能(Concurrent Features)
- Tailwind CSSによるCSS最適化

### 開発体験の向上
- 最新のTypeScript機能
- 最新のReact開発ツール
- Tailwind CSSの開発効率

### メンテナンス性
- 最新のセキュリティ更新
- 長期サポート版の使用
- モダンな開発パターンの採用

## 注意事項

- Firebase設定ファイルにAPIキーが含まれているため、セキュリティ設定の確認が必要
- デプロイスクリプトの `build` ディレクトリ参照を `dist` に変更する必要
- 本番環境でのテストが必要

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ