Reactで画面を実装してみた

[前回](https://qiita.com/betarium/items/ff0b719276d381752efe)まででReactとExpressの両方が動作する開発環境を作成したので、次はReactで画面を複数追加し、画面遷移できるようにしてみる。

:::note
[前回：VScode+Dockerでexpressの開発環境を共有してみた](https://qiita.com/betarium/items/ff0b719276d381752efe)
:::

https://qiita.com/betarium/items/ff0b719276d381752efe

ここでは例としてユーザー一覧とユーザー詳細画面を表示できるようにする。

# 画面の追加

複数の画面を表示するにはURLのパスに合わせて画面を切り替えて表示する仕組みが必要である。reactでは`react-router`、`react-router-dom`というパッケージが用意されているので、まずはこれを追加する。

```sh
cd front
pnpm install react-router
pnpm install react-router-dom
```

## main.tsx

importを追加する。

```main.tsx
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import UsersListPage from './pages/UsersListPage'
```

`<React.StrictMode>`に囲まれた個所を以下のように修正する。
react-routerの機能により`path`に指定されたパスに一致する画面が表示されるようになる。

```main.tsx
    <BrowserRouter>
      <Routes>
        <Route path="/users" element={<UsersListPage />} />
        <Route path="/" element={<App />} />
      </Routes>
    </BrowserRouter>
```

## App.tsx

先頭にimportを追加する。

```App.tsx
import { useCallback } from 'react'
import { useNavigate } from 'react-router'
import { MouseEvent } from 'react'
```

:::note
MouseEventをimportしないとonClickの箇所でビルドエラーになる。
:::

HTMLの箇所にリンクを追加する。
onClickに`onClickUserListLink`を指定することにより、リンクをクリックされたら`onClickUserListLink`を呼び出すようにしている。

```App.tsx
      <div>
        <a href="/users" onClick={onClickUserListLink}>User List</a>
      </div>
```

HTMLの上のコードに、リンクをクリックされたときの処理を追加する。
関数定義はパフォーマンス向上のため`useCallback`でラップしている。（メモ化というらしい）
ボタンやリンクを押した場合の画面遷移をスキップするため`preventDefault`を呼んでいる。
画面遷移には`useNavigate`から取得した関数にパスを指定して呼び出す。

```App.tsx
  const navigate = useNavigate()

  const onClickUserListLink = useCallback((e: MouseEvent) => {
    e.preventDefault()
    navigate("/users")
  }, [navigate])
```

修正後のコードは以下のようになる。

```App.tsx
import { useCallback } from 'react'
import reactLogo from './assets/react.svg'
import './App.css'
import { useNavigate } from 'react-router'
import { MouseEvent } from 'react'

function App() {
  const navigate = useNavigate()

  const onClickUserListLink = useCallback((e: MouseEvent) => {
    e.preventDefault()
    navigate("/users")
  }, [navigate])

  return (
    <div className="App">
      <div>
        <a href="https://vitejs.dev" target="_blank">
          <img src="/vite.svg" className="logo" alt="Vite logo" />
        </a>
        <a href="https://reactjs.org" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div>
        <a href="/users" onClick={onClickUserListLink}>User List</a>
      </div>
    </div>
  )
}

export default App
```

## pages/UsersListPage.tsx

ユーザー一覧画面（の予定）を追加する。

こちらには一覧からトップページへ戻るリンクを追加する。
トップページと同様、リンクがクリックされたら`onClickTopLink`を呼び出し、トップページに画面遷移する。

```pages/UsersListPage.tsx
import React, { MouseEvent, useCallback } from 'react'
import { useNavigate } from 'react-router'

function UsersListPage() {
  const navigate = useNavigate()

  const onClickTopLink = useCallback((e: MouseEvent) => {
    e.preventDefault()
    navigate("/")
  }, [navigate])

  return (
    <>
      <h1>User List</h1>
      <div style={{ textAlign: "left" }}>
        <a href="/" onClick={onClickTopLink}>top...</a>
      </div>
    </>
  )
}

export default UsersListPage
```

----

ここまで出来たらフロントのデバッグサーバーを起動して動作を確認してみる。

```
cd front
pnpm dev
```

ブラウザで http://localhost:3000/ を開いて、トップページからUser Listリンクをクリックする。ユーザー一覧画面（の予定）が表示されれば成功である。
SPA(Single Page Application)として作成しているので、画面遷移してもページの再読み込みをしないことがブラウザのデバッグコンソールで確認できる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/372771/518831ec-10c3-a666-9469-da986e738a6c.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/372771/dbc76dd3-3300-f2e7-61c8-65cd0545dd5e.png)

# ユーザー一覧画面とユーザー詳細画面の作成

続いて、ユーザー詳細画面を追加しユーザー一覧画面から画面遷移できるようにする。

## main.tsx

`<Routes>`の箇所にユーザー詳細画面の`<Route>`を追加する。
`path`に`:id`のように記述することにより、パスパラメータとして画面側でパスの一部をパラメータとして取得できる。

```main.tsx
        <Route path="/users/:id" element={<UsersDetailPage />} />
```

## models/UserModel.ts

ユーザーデータを管理するモデルを作成する。

```models/UserModel.ts
export default interface UserModel {
    id: number
    account: string
    password?: string
}
```

動作確認のため、テストデータも定義しておく。

```models/UserModel.ts
const user1 = { id: 1, account: "admin", password: "hogehoge" } as UserModel
const user2 = { id: 2, account: "guest", password: "piyopiyo" } as UserModel

export const testUserList = [user1, user2]
```

## UsersListPage.tsx

importを追加する。
viteで生成したプロジェクトではCSSファイルをインポートで読み込めるよう設定されてるので、別途作成したCSSファイルを読み込む。(CSSファイルは省略。github参照)

```UsersListPage.tsx
import UserModel, { testUserList } from '../models/UserModel'
import "./Users.css"
```

リンクをクリックされた時の処理を追加する。
`onClickUserDetailLink`ではユーザー詳細画面に遷移する際、パスにユーザーIDを含めて指定している。ユーザー詳細画面ではこのIDを取得してユーザーごとの情報を表示する。

```UsersListPage.tsx
  const userList = testUserList

  const onClickUserDetailLink = useCallback((e: MouseEvent, user: UserModel) => {
    e.preventDefault()
    navigate("/users/" + user.id)
  }, [navigate])

  const onClickUserCreateLink = useCallback((e: MouseEvent) => {
    e.preventDefault()
    alert("create!!")
  }, [])

  const onClickUserDelete = useCallback((e: MouseEvent, user: UserModel) => {
    e.preventDefault()
    alert(`${user.account} delete!!`)
  }, [])
```

リンクとユーザー一覧のテーブルを追加する。
一覧などを表示する場合、reactのJSXの箇所ではfor文が使用できないので、かわりに`Array.prototype.map`メソッドを使用して配列中の要素を取り出し順次処理する。

```UsersListPage.tsx
      <div style={{ textAlign: "left" }}>
        <a href="/users/new" onClick={onClickUserCreateLink}>create...</a>
      </div>
      <section style={{ minWidth: "400px" }}>
        <table style={{ width: "100%" }}>
          <thead>
            <tr>
              <th>ID</th>
              <th>Account</th>
              <th>Detail</th>
              <th>Delete</th>
            </tr>
          </thead>
          <tbody>
            {userList.map(item =>
              <tr key={`user-${item.id}`}>
                <td>{item.id}</td>
                <td>{item.account}</td>
                <td><a href={`/users/${item.id}`} onClick={e => onClickUserDetailLink(e, item)}>[Detail]</a></td>
                <td><a href={`/users/${item.id}`} onClick={e => onClickUserDelete(e, item)}>[Delete]</a></td>
              </tr>
            )}
          </tbody>
        </table>
      </section>
```

## UsersDetailPage.tsx

ユーザー詳細画面を追加する。
`main.tsx`で指定したパスパラメータにより、パスから`id`を取得し、idをもとにユーザー情報を表示している。
`input`に値を表示するには`value={変数}`として指定する。

```UsersDetailPage.tsx
import React, { MouseEvent, useCallback } from 'react'
import { useNavigate, useParams } from "react-router";
import { testUserList } from '../models/UserModel'
import "./Users.css"

function UsersDetailPage() {
  const navigate = useNavigate()

  const params = useParams()
  const userId = parseInt(params["id"]!!)

  const user = testUserList.filter(p => p.id === userId)[0]

  const onClickUserListLink = useCallback((e: MouseEvent) => {
    e.preventDefault()
    navigate("/users")
  }, [navigate])

  const onSave = useCallback(async () => {
    alert("save!!")
  }, [])

  return (
    <>
      <h1>User Detail</h1>
      <div style={{ textAlign: "left" }}>
        <a href="/users" onClick={onClickUserListLink}>list...</a>
      </div>
      <section style={{ minWidth: "400px" }}>
        <dl>
          <dt>ID</dt>
          <dd>
            {user.id}
          </dd>
          <dt>Account</dt>
          <dd>
            <input type="text" value={user.account} />
          </dd>
          <dt>Password</dt>
          <dd>
            <input type="text" value={user.password} />
          </dd>
        </dl>
        <div>
          <input type="submit" value="Save" onClick={onSave} />
        </div>
      </section>
    </>
  )
}

export default UsersDetailPage
```

----

以上で完成である。
デバッグサーバを起動してユーザー一覧からユーザー詳細画面が表示できれば成功である。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/372771/e4530ce4-328f-a0ff-1c42-fea4dd20ca07.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/372771/e6b4a76b-61ad-0a2c-86a1-a0646c630290.png)

createやdelete、saveのリンクやボタンは動作しないが、実際のシステムではAPI経由でデータを取得したり更新する処理を実装していくことになる。

というわけで、次回の記事でAPI呼び出しの実装について説明する予定である。

# サンプルコード
[https://github.com/betarium/sample-react-express/tree/0.3.0](https://github.com/betarium/sample-react-express/tree/0.3.0)