
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、このところReact
をメインにして記事を公開していますが、使えば使うほどにその楽しさに気がついている今日この頃です。
とは言え、Vue
を長く使っている私からすると「うーん、この機能は React にないのか…」となるものがあったりします。
それは・・・・・・
コンポーネントの名前付きスロット
です。
例えば、Vue
の場合だと以下のようにすると、
<v-your-component>
<template v-slot:header>ここはヘッダーで表示</template>
<template v-slot:footer>ここはフッターで表示</template>
</v-your-component>
それぞれ場所を分けて表示することができます。
<div class="container">
<slot name="header"></slot>
<hr />
<slot name="footer"></slot>
</div>
もちろん通常通りパラメータを切り分けても問題ないのですが、Vue
の開発が長かったこともあり「スロット使いたいな」という気持ちになったんですね。
そこで
今回は、React
でVue
のような名前付きスロットを使ってみることにします。
ぜひ何かの参考になりましたら嬉しいです
「カナダの
ヒマワリの種スナック
いただきました」
開発環境: Laravel 9.x、React、Vite、Intertia.js、TailwindCSS
ルートをつくる
まずは今回使うコードのためにルートをつくります。
routes/web.php
// 省略
// Component Slot
Route::get('component_slot', fn() => Inertia::render('ComponentSlot/Index'));
ビュー(テンプレート)をつくる
続いて、先ほど指定した「ComponentSlot/Index」に該当するビュー(テンプレート)をつくります。
resources/js/Pages/ComponentSlot/Index.jsx
import FullNameCard from "@/Components/FullNameCard";
export default function Index() {
return (
<div className="w-96 p-5">
<FullNameCard>
<template slot="first_name">太郎</template>
<template slot="last_name">山田</template>
</FullNameCard>
</div>
);
}
なお、この中の、<template slot="*****"> ... </template>
と書かれている中身がそれぞれコンポーネント内で表示されることになります。
コンポーネントをつくる
ビューの中で指定した「FullNameCard」コンポーネントがまだ存在していないので、以下のようにします。
resources/js/Components/FullNameCard.jsx
import {Children} from "react";
export default function FullNameCard(props) {
const children = Children.toArray(props.children); // 必ずスロットを配列で取得する
const firstName = children.find(child => {
return (
child.type === 'template' &&
child.props.slot === 'first_name' // スロット名が一致する要素を返す
);
}).props.children || '';
const lastName = children.find(child => {
return (
child.type === 'template' &&
child.props.slot === 'last_name' // スロット名が一致する要素を返す
);
}).props.children || '';
return (
<div className="flex p-0.5 border bg-gray-100">
<div className="flex-1 border p-1 bg-white">
<div className="text-xs font-bold">名</div>
<span>{firstName}</span>
</div>
<div className="flex-1 border p-1 bg-white">
<div className="text-xs font-bold">姓</div>
<span>{lastName}</span>
</div>
</div>
);
}
この中でやっているのは、先ほどの<template slot="*****"> ... </template>
の中身を取得して表示しているだけです。
※なお、find()
の最後についている || ''
は、「もし無かったら空白ね」という意味になります。
より汎用的にスロット機能が使えるようにする
すでに今回の機能としては完成していますが、もしかすると「名前付きスロット」は他のコンポーネントでも使いたい場合がでてくるかもしれませんよね
毎回先ほどのコードをコピペしてもいいですが、できれば1つにまとめておき、「いつでもどこでも」簡単に使えるようにしたいものです。
ということで、今回はスロットを取得する部分を共通化し、それをヘルパー関数(今回はクラス)にしてみましょう。
resources/js/Helper/Component.jsx
import {Children} from "react";
export class ComponentSlot {
constructor(children) {
this.children = Children.toArray(children);
}
get(key, defaultValue='') {
const contents = this.children.find(child => {
return (
child.type === 'template' &&
child.props.slot === key // スロット名が一致する要素を返す
);
});
return contents
? contents.props.children
: defaultValue; // コンテンツがない場合はデフォルト値を返す
}
}
コードを見ていただくと分かると思いますが、先ほどのコードをクラス化しただけです。
なお、ひとつ違うのはget()
で初期値を設定できるようにしている部分(defaultValue
)です。つまり、もし指定されたスロットが存在していない場合にその初期値が表示されるようになります。
では、ヘルパークラスが完了したら、早速先ほどのFullNameCard
コンポーネントで使ってみましょう。
resources/js/Components/FullNameCard.jsx
import {ComponentSlot} from "@/Helper/Component";
export default function FullNameCard(props) {
const slot = new ComponentSlot(props.children);
const firstName = slot.get('first_name', '-');
const lastName = slot.get('last_name', '-');
return (
// 省略
);
}
コードがすっきりしました。
使い方は、コンストラクタにprops.children
を指定し、get()
を呼び出すだけです。
これでもし別のコンポーネントをつくった場合でもComponentSlot
を呼び出すだけで「名前付きスロット機能」が使えるようになります。
テストしてみる
では、実際にテストしてみましょう
まずはVite
を起動して「http://******/component_slot」へブラウザでアクセスします。
すると・・・・・・
はい
スロットの中身がうまく表示されました。
では、次にスロットの中身を以下のように変更して確認してみます。
(last_name
の部分にはCSS
が当たっていることに注目してください)
<FullNameCard>
<template slot="first_name">次郎</template>
<template slot="last_name">
<strong className="text-blue-600 text-lg">佐藤</strong>
</template>
</FullNameCard>
どうなったでしょうか・・・・・・
はい
今度は「佐藤次郎」さんの名前が表示され、さらに色と大きさを変更した部分もうまく表示されています。
では、最後に「リアクティブ」に名前を変更できるか確認しておきましょう。
コードは以下のようにしました。(クリックしたら、佐藤次郎 ➜ 田中三郎 へ変数の中身を変更します)
resources/js/Pages/ComponentSlot/Index.jsx
import { useState } from "react";
import FullNameCard from "@/Components/FullNameCard";
export default function Index() {
const [firstName, setFirstName] = useState('次郎');
const [lastName, setLastName] = useState('佐藤');
const handleClick = () => {
setFirstName('三郎');
setLastName('田中');
};
return (
<div className="w-96 p-5">
<FullNameCard>
<template slot="first_name">{firstName}</template>
<template slot="last_name">
<strong className="text-blue-600 text-lg">{lastName}</strong>
</template>
</FullNameCard>
<button type="button" className="text-white bg-blue-700 rounded-lg text-sm px-4 py-2" onClick={handleClick}>名前を変更</button>
</div>
);
}
ちなみに表示は次のようになります。
では、「名前を変更」ボタンをクリックするとどうなるでしょうか??
はい
名前がそれぞれ変更されました。
成功です
企業様へのご提案
React
とVue
は比較的コンセプトが似ているフレームワークだと思いますが、やはり「どちらかでは使えるのに、一方では使えない」機能が存在しています。
そのため、もし「Vue ➜ React」もしくは、「React ➜ Vue」へ移行する際は今回のようなテクニックを使うと学習コストを下げることができるかと思います。
もしそういった「フレームワークのお引越し」作業をされる場合はぜひお力になりますので、お問い合わせよりお気軽にご相談ください。
お待ちしております。m(_ _)m
おわりに
ということで、今回はReact
内でVue
の名前付きスロット機能を使ってみました。
そもそも本家からは提供されてはいない機能ですが、こうやって工夫することで以前と同じような機能をつくることができます。
しかも今回この機能を作るにあたってひとつ勉強になったのが、「ReactのバーチャルDOMは、結局HTMLをわかりやすくオブジェクト化したものなんだ」ということです。
名前からは気づきませんでしたが、そうなるとまた別のことが実現できるような気がします。
ぜひ皆さんも楽しく探ってみてくださいね。
ではでは〜
「楽しかった会食の
次の日、
その空虚感たるや…」