laravel + inertia + react のbootcampを見る (その3: Chirpsを表示する)

https://bootcamp.laravel.com/inertia/showing-chirps

さて、今、投稿の保存が終了した。これを表示してみよう。(一応この段階でgit をコミットしてある)

indexをさらに改造していく

ChirpモデルにbelongsToを与える

今chirpsの内容であるが

      App\Models\Chirp {#7233
        id: 1,
        user_id: 1,
        message: "テスト",
        created_at: "2023-08-03 23:59:37",
        updated_at: "2023-08-03 23:59:37",
      },

このようになっていた。このuser_idから当該のUserモデルの内容を取り出す場合ばbelongsToであるが、それが定義されていないので行う

use Illuminate\Database\Eloquent\Relations\BelongsTo;
//...
class Chirp extends Model
{
    use HasFactory;

    protected $fillable = [
        'message',
    ];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

}

Controllerから取り出して注入する

app/Http/Controllers/ChirpController.php

    public function index(): Response
    {
        return Inertia::render('Chirps/Index', [
            'chirps' => Chirp::with('user:id,name')->latest()->get(),
        ]);
    }

これはいろいろな理由でwithになっているんだけど、リレーション先Userモデルのidとnameも同時に取得してしまうという事である。とりあえずよくわからなければそういうものと思ってok。latestはcreated_atとかでorder by descしたのと同じ。

これを配列に与えてinertiaに注入する。これはBladeでも散々やった事と思う。

resources/js/Pages/Chirps/Index.jsxで表示する

現状のコードをまず書いておく

import React from 'react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import InputError from '@/Components/InputError';
import PrimaryButton from '@/Components/PrimaryButton';
import { useForm, Head } from '@inertiajs/react';

export default function Index({ auth }) {
    const { data, setData, post, processing, reset, errors } = useForm({
        message: '',
    });

    const submit = (e) => {
        e.preventDefault();
        post(route('chirps.store'), { onSuccess: () => reset() });
    };

    return (
        <AuthenticatedLayout user={auth.user}>
            <Head title="Chirps" />

            <div className="max-w-2xl mx-auto p-4 sm:p-6 lg:p-8">
                <form onSubmit={submit}>
                    <textarea
                        value={data.message}
                        placeholder="What's on your mind?"
                        className="block w-full border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm"
                        onChange={e => setData('message', e.target.value)}
                    ></textarea>
                    <InputError message={errors.message} className="mt-2" />
                    <PrimaryButton className="mt-4" disabled={processing}>Chirp</PrimaryButton>
                </form>
            </div>
        </AuthenticatedLayout>
    );
}

ここに今chirpsというオブジェクト配列が渡されているので、これを受けとる必要がある。それは

export default function Index({ auth }) {

これを

export default function Index({ auth, chirps }) {

にする事で達成できる。

これをループ(イテレーション)してみよう

    return (
        <AuthenticatedLayout user={auth.user}>
            <Head title="Chirps" />

            <div className="max-w-2xl mx-auto p-4 sm:p-6 lg:p-8">
                <form onSubmit={submit}>
                    <textarea
                        value={data.message}
                        placeholder="What's on your mind?"
                        className="block w-full border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm"
                        onChange={e => setData('message', e.target.value)}
                    ></textarea>
                    <InputError message={errors.message} className="mt-2" />
                    <PrimaryButton className="mt-4" disabled={processing}>Chirp</PrimaryButton>
                </form>

                {/* ここから */}
                <div className="mt-6 bg-white shadow-sm rounded-lg divide-y">
                    {chirps.map(chirp =>
                        <div className="p-6 flex space-x-2">
                            <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-600 -scale-x-100" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
                                <path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
                            </svg>
                            <div className="flex-1">
                                <div className="flex justify-between items-center">
                                    <div>
                                        <span className="text-gray-800">{chirp.user.name}</span>
                                        <small className="ml-2 text-sm text-gray-600">{new Date(chirp.created_at).toLocaleString()}</small>
                                    </div>
                                </div>
                                <p className="mt-4 text-lg text-gray-900">{chirp.message}</p>
                            </div>
                        </div>
                    )}
                </div>
                {/* ここまで */}

            </div>
        </A

このように追記すると、以下のように表示れるはずだ。

いくらか書いてみると良いと思う。

投稿をコンポーネント化する

reactの場合割とどんどんコンポーネント化するのが作法なので、先程の投稿の部分をコンポーネントにしてみる

                <div className="mt-6 bg-white shadow-sm rounded-lg divide-y">
                    {chirps.map(chirp =>
                        <div className="p-6 flex space-x-2">
                            <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-600 -scale-x-100" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
                                <path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
                            </svg>
                            <div className="flex-1">
                                <div className="flex justify-between items-center">
                                    <div>
                                        <span className="text-gray-800">{chirp.user.name}</span>
                                        <small className="ml-2 text-sm text-gray-600">{new Date(chirp.created_at).toLocaleString()}</small>
                                    </div>
                                </div>
                                <p className="mt-4 text-lg text-gray-900">{chirp.message}</p>
                            </div>
                        </div>
                    )}
                </div>

対象はmapで回ってる中身という事になる。まあmapごとコンポーネントにしてもいいかもしれないが少なくともここはbootcampのドキュメントに従う事にする(今更だが)

ここではドキュメントの通り resources/js/Components/Chirp.jsx を作成する

import React from 'react';

export default function Chirp({ chirp }) {
    return (
        <div className="p-6 flex space-x-2">
            <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-600 -scale-x-100" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
                <path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
            </svg>
            <div className="flex-1">
                <div className="flex justify-between items-center">
                    <div>
                        <span className="text-gray-800">{chirp.user.name}</span>
                        <small className="ml-2 text-sm text-gray-600">{new Date(chirp.created_at).toLocaleString()}</small>
                    </div>
                </div>
                <p className="mt-4 text-lg text-gray-900">{chirp.message}</p>
            </div>
        </div>
    );
}

内容はほぼコピペだが

export default function Chirp({ chirp }) {

ここでChirpモジュールを定義してchirpという引数を受けとっている

そしたらIndex.jsxではこのようにしよう。ちょっと統一感を出してみたけど順番はまあどうでもいいかも

import React from 'react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { useForm, Head } from '@inertiajs/react';

import InputError    from '@/Components/InputError';
import PrimaryButton from '@/Components/PrimaryButton';
import Chirp         from '@/Components/Chirp';

のようにimportできる。ここから理解できるようにInputErrorやPrimaryButtonなども全てコンポーネントになっており、その一覧はこのように確認できるはずだ

% find resources/js/Components
resources/js/Components
resources/js/Components/Checkbox.jsx
resources/js/Components/Dropdown.jsx
resources/js/Components/InputLabel.jsx
resources/js/Components/InputError.jsx
resources/js/Components/ApplicationLogo.jsx
resources/js/Components/NavLink.jsx
resources/js/Components/TextInput.jsx
resources/js/Components/Chirp.jsx
resources/js/Components/PrimaryButton.jsx
resources/js/Components/DangerButton.jsx
resources/js/Components/SecondaryButton.jsx
resources/js/Components/Modal.jsx
resources/js/Components/ResponsiveNavLink.jsx

ここでresources/js/Pages/Chirps/Index.jsxをアップデートする(抜粋)

                <div className="mt-6 bg-white shadow-sm rounded-lg divide-y">
                    {chirps.map(chirp =>
                        <Chirp key={chirp.id} chirp={chirp} />
                    )}
                </div>

このようにする事で表示されるはずだ。ここでのkeyは今のところ「とりあえず与えておく」としといてもよい。

なお、後半ではdayjsの使い方が書かれているが、これはやってもやらなくてもok。ただ、重要なのはbladeでやっていたようなCarbonを使った変換はできないという事を覚えておく事が重要である。



この記事が気に入ったらサポートをしてみませんか?