『React ハンズオンラーニング』(原著 2020 年) の recipes-app を今どきの環境で動かす

『React ハンズオンラーニング 第2版』(オライリー) は、特に他言語の経験のあるプログラマーにとって良いガイドです。

しかし、原著が 2020 年に出版されてからの JavaScript 技術の発展により、記述の古さも否めません。

初心者が Vite (TypeScript + SWC) で動かしてみたので、やったことを共有します。Vite を使っているのは Create React App が動かなかった時に検索したら、[It looks like create-react-app is dead. What should I use instead?](https://www.reddit.com/r/reactjs/comments/12x6k9x/it_looks_like_createreactapp_is_dead_what_should/) でおすすめされていたためです。ちなみに Vite はヴィートと読みます。

ご紹介するのは以下の 2 つです。

- Vite で動かすまでの手順
- 元の recipes-app を動かす方法


## Vite で動かすまでの手順

1. npm create vite@latest を実行し、プロジェクト名 (サブディレクトリーになる) を入力後、React > TypeScript + SWC を選択してください。SWC は Babel の代替の Rust 実装で、高速です。
1. npm install します。
1. 以後プロジェクトのルート ディレクトリーを `~` と表記しますが、`~/data/` に元の実装の recipes.json を配置します。
1. `~/src/` に元の実装の components ディレクトリーを配置します。`~/src/components/Ingredient.js` などができます。
1. main.tsx の内容を、元の実装の index.js と同等になるように、以下の内容にします。
   ```tsx
   import { StrictMode } from 'react'
   import { createRoot } from 'react-dom/client'
   import data from "../data/recipes.json";
   import Menu from './components/Menu'
   
   createRoot(document.getElementById('root')!).render(
     <StrictMode>
       <Menu recipes={data} />
     </StrictMode>,
   )
   ```
1. components の .js ファイルを .tsx に変更します。
1. 型を定義し、引数に型を記述します。GitHub Copilot が大体いい感じに書いてくれます。例えば Ingredient.tsx は以下のようになります。
   ```tsx
   export interface IngredientProps {
     amount: number;
     measurement: string;
     name: string;
   }
   
   export function Ingredient({ amount, measurement, name }: IngredientProps) {
     return (
       <li>
         {amount} {measurement} {name}
       </li>
     );
   }
   ```
1. npm run dev で動作するはずです。


### type か interface か

上記の IngredientProps を type とするか interface とするかは判断の分かれるところです。『React ハンズオンラーニング』の 10.3.3 節では AppProps を type として定義した例が記載されていますが、私は『TypeScript と React/Next.js でつくる実践 Web アプリケーション開発』(技術評論社) の以下の記述を判断基準としました。

> オブジェクトの型を定義する際にインタフェースと型エイリアスどちらも利用が可能で、継承に関する細かな機能の違いはあるもののほぼ同等の機能を持ちます。
> 
> ただし、TypeScript の設計思想としてこの 2 つの機能は少し異なる点があります。
> 
> インタフェースはクラスやデータの一側面を定義した型、つまり、インタフェースにマッチする方でもその値以外にほかのフィールドやメソッドがある前提でのものです。一方、型エイリアスはオブジェクトの型そのものを表すものです。
> 
> オブジェクトそのものではなく、クラスやオブジェクトの一部のプロパティや関数含む一部の振る舞いを定義するものであれば、インタフェースを利用するのが適していると言えます。

recipes-app の場合、recipes.json のデータと、その使われ方はぴったり一致しているのでオブジェクトそのものを定義すると考え、type で定義する人もいるでしょう。しかし、別のサーバーの API から返される値だと考えれば、API のバージョンが上がってプロパティが追加された際に書き換える範囲が少ないほうが嬉しいのではないかと私は思います。このため、interface としました。

また、[TypeScript with React Components](https://react.dev/learn/typescript#typescript-with-react-components) で interface が使われていたことも、interface を採用した理由です。


## 元の recipes-app を動かす方法

元の recipes-app は Windows では動かず、また最新の Node.js では動かなくなっているようです。

### Windows で発生するエラー

```console
npm run build

> recipe-app@1.0.0 build
> npm run clean && mkdir ./dist && cp ./index.html ./dist/ && webpack --mode development


> recipe-app@1.0.0 clean
> rm -fR ./dist

'rm' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。
```

Windows の npm は、たとえ Git Bash から実行しても、cmd.exe で実行されるようです。このため、rm コマンドが使えず `npm run build` が動きません。

Docker、WSL などの Linux 環境を使う必要があります。私は慣れている WSL を使いました。

### 最新の Node.js で発生するエラー

`npm run build` や `npm start` で以下のエラーが発生します。

```console
Error: error:0308010C:digital envelope routines::unsupported
    at new Hash (node:internal/crypto/hash:79:19)
    at Object.createHash (node:crypto:139:10)
    : (省略)
  at /path/to/learning-react-2e-ja/chapter-05/5.3/recipes-app/node_modules/babel-loader/lib/index.js:59:71 {
  opensslErrorStack: [
    'error:03000086:digital envelope routines::initialization error',
    'error:0308010C:digital envelope routines::unsupported'
  ],
  library: 'digital envelope routines',
  reason: 'unsupported',
  code: 'ERR_OSSL_EVP_UNSUPPORTED'
}    
```

これは `NODE_OPTIONS=--openssl-legacy-provider` を環境変数に定義することで解消します。グローバルに変更するのは好ましくないと思いますので、bash なら以下のように npm コマンドの前につけて実行するのがおすすめです。

```console
$ NODE_OPTIONS=--openssl-legacy-provider npm run build
$ NODE_OPTIONS=--openssl-legacy-provider npm start
```

----

以上、少し不親切な記述となっていますが、どなたかの参考になれば幸いです。