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を使った変換はできないという事を覚えておく事が重要である。