Inertia.jsのPartial reloadsを理解したい

# ■ Partial reloads

Partial Reloadsは、Inertia.jsの強力な機能の一つで、ページの特定の部分だけを更新することができます。これにより、全ページを再読み込みする必要がなくなり、ユーザーエクスペリエンスが向上します。たとえば、ユーザーがフォームを送信した後にリストのみを更新したい場合や、特定のコンポーネントのデータを更新したい場合に便利です。

https://inertiajs.com/partial-reloads

本記事では、Laravel×ReactのコードベースでPartial reloadsの動きを確認します。

# ■ Partial reloadsでページの一部だけを更新する

ユーザー検索機能を例にPartial reloadsでページの一部だけを更新する場合の実装を確認していきます。Inertia自体にそこまで馴染みがない方もいると思うので、順を追って丁寧に機能を実装していきます。

### ▼ 準備

まずはユーザー検索機能を付けずに、Inertiaを利用したシンプルなユーザー一覧ページを用意します。

```React:resouces/js/Pages/User/List.tsx
type Props = {
  users: {
    id: number;
    name: string;
  }[];
};

export default function List(props: Props) {
  const { users } = props;

  return (
    <div>
      <h1>ユーザー一覧</h1>
      <ul>
        {users.map((user) => <li key={user.id}>{user.name}</li>)}
      </ul>
    </div>
  );
}
```

```php:app/Http/Controllers/User/List/Controller.php
<?php

namespace App\Http\Controllers\User\List;

use Inertia\Inertia;
use App\Models\User;

class Controller {
    public function __invoke() {
        $users = User::all();

        return Inertia::render("User/List", [
            "users" => $users,
        ]);
    }
}
```

```php:routes/web.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\User;

Route::get('/user', User\List\Controller::class);
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1166480/fa0dbbda-aeef-9e28-3536-7e62b345df23.png)

### ▼ ユーザー検索機能追加

この一覧ページにユーザーの名前検索機能を追加していきます。検索時にPartial reloadsすることで、ページを再読み込みすることなく、ユーザー名部分だけ更新することができます。

まずは、ユーザー一覧のリクエストクラスを追加し、コントローラー部分を修正して、検索処理を追加します。

```php:app/Http/Controllers/User/List/Request.php
<?php

namespace App\Http\Controllers\User\List;

use Illuminate\Foundation\Http\FormRequest;

class Request extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'name' => 'sometimes|string|max:255',
        ];
    }
}

```

```diff_php:app/Http/Controllers/User/List/Controller.php
<?php
  
namespace App\Http\Controllers\User\List;
  
use Inertia\Inertia;
use App\Models\User;
  
class Controller {
-   public function __invoke() {
+   public function __invoke(Request $request) {

+       $search_name = $request->query("name");

-       $users = User::all();
+       $users = User::query()
+           ->when($search_name, fn ($query, $search_name) => $query->where("name", "like", "%{$search_name}%"))
+           ->get();

        return Inertia::render("User/List", [
            "users" => $users,
        ]);
    }
}
```



次に、フロントエンド側の準備です。検索フォームコンポーネントを用意します。

```react:resources/js/Components/User/List/SearchForm.tsx
import { useState } from 'react';

type Props = {
  handleSearch: (name: string) => void;
}

export default function SearchForm(props: Props) {
  const { handleSearch } = props;

  const [name, setName] = useState('');

  const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setName(e.target.value);
  };

  return (
    <div>
      <input type="text" value={name} onChange={handleNameChange} />
      <button onClick={() => handleSearch(name)}>検索</button>
    </div>
  );
}
```

これを使用して、検索機能を完成させます。

```diff_tsx:resouces/js/Pages/User/List.tsx
+ import SearchForm from "@/Components/User/List/SearchForm";
+ import { router } from "@inertiajs/react";

type Props = {
  users: {
    id: number;
    name: string;
  }[];
};

export default function List(props: Props) {
  const { users } = props;

+ const handleSearch = (name: string) => {
+   router.reload({
+     data: { name }, // クエリパラメータにnameを追加
+     only: ['users'], // usersのみ再取得 👈 Partial reloads！
+   })
+ };

  return (
    <div>
      <h1>ユーザー一覧</h1>
+     <SearchForm handleSearch={handleSearch} />
      <ul>
        {users.map((user) => <li key={user.id}>{user.name}</li>)}
      </ul>
    </div>
  );
}
```


![画面収録 2024-02-12 0.09.22.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1166480/2413fa9e-ce4d-077b-3dff-cafa57fd2046.gif)

# ■ Lazy data evaluation

Lazy data evaluationについて解説します。説明のために、次のような２つのサイコロの目を振るページを用意しました。

![画面収録 2024-02-12 0.46.18.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1166480/32c00203-7fae-6a1f-1f23-ca87a77ac3e4.gif)


```tsx:resources/js/Dice.tsx
import { router } from "@inertiajs/react";

type Props = {
  dice1: number;
  dice2: number;
};

export default function Dice(props: Props) {
  const { dice1, dice2 } = props;

  const rollDice1 = () => {
    router.reload({
      only: ['dice1'],
    });
  };

  const rollDice2 = () => {
    router.reload({
      only: ['dice2'],
    });
  }

  return (
    <>
      <p>サイコロ1: {dice1}</p>
      <button onClick={rollDice1}>サイコロ1を振り直す</button>
      <p>サイコロ2: {dice2}</p>
      <button onClick={rollDice2}>サイコロ2を振り直す</button>
    </>
  );
}
```

```php:app/Http/Controllers/Dice/Controller.php
<?php

namespace App\Http\Controllers\Dice;

use Illuminate\Support\Facades\Log;
use Inertia\Inertia;

class Controller {
    public function __invoke() {
        $roll1 = function () {
            Log::info("サイコロ1が振られました");
            return random_int(1,6);
        };

        $roll2 = function () {
            Log::info("サイコロ2が振られました");
            return random_int(1,6);
        };

        return Inertia::render("Dice", [
            "dice1" => $roll1(),
            "dice2" => $roll2(),
        ]);
    }
}
```

```php:routes/web.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Dice;

Route::get("/dice", Dice\Controller::class);
```

さて、どちらもPartial reloadsでデータを取得しているので、サイコロ1を振り直せばサイコロ1の目だけが変わります。サイコロ2についても同様です。しかし、ログを確認すると、どちらのサイコロも振られています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1166480/8465748c-1d30-e717-fd4b-810251ad75a0.png)

つまり、現状だと、`$roll1`、`$roll2`がともに実行されてから、どのデータだけを返却するのか評価していることになります。

```php:app/Http/Controllers/Dice/Controller.php
// ...

class Controller {
    public function __invoke() {
        // ...

        return Inertia::render("Dice", [
            "dice1" => $roll1(), // 👈　 $roll1が実行されてからdice1を返すかどうか評価している
            "dice2" => $roll2(), // 👈　 $roll2が実行されてからdice2を返すかどうか評価している
        ]);
    }
}
```

サイコロ1のデータしか必要ないときに、サイコロ2も振ってしまうのは無駄ですから、振らないようにしたいです。そこで、どちらサイコロのデータが必要なのか先に判断し、そのサイコロだけのデータを取得するようにします。このように、データが必要になったときだけ、データ取得処理が実行されるようにする方法がLazy data evaluationです。これができれば無駄な処理が実行されずに済みます。

実装方法は簡単で、データ取得処理をクロージャーで包むだけです。

```php:app/Http/Controllers/Dice/Controller.php
// ...

class Controller {
    public function __invoke() {
        // ...

        return Inertia::render("Dice", [
            "dice1" => fn () => $roll1(), // 👈　 $dice1を返す場合のみ$roll1が実行される
            "dice2" => fn () => $roll2(), // 👈　 $dice2を返す場合のみ$roll2が実行される
        ]);
    }
}
```

こうすることで、必要なデータ取得処理だけが実行できるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1166480/7485ede5-74ed-d1e6-6d63-978103287072.png)

さらに、初期表示時には、データを取得したくないというケースもあるでしょう。そういう場合はさらに、`Inertia::lazy()`でデータ取得部分をラッピングします。

```php:app/Http/Controllers/Dice/Controller.php
// ...

class Controller {
    public function __invoke() {
        // ...

        return Inertia::render("Dice", [
            "dice1" => Inertia::lazy(fn () => $roll1()),
            "dice2" => Inertia::lazy(fn () => $roll2()),
        ]);
    }
}
```

![画面収録 2024-02-12 1.19.45.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1166480/d3b43078-7d9e-9a18-6d93-fc566cfae412.gif)