React講座 要素のレンダー

React要素の使い方

要素(エレメント)とは React アプリケーションの最小単位の構成ブロックです。 ブラウザのDOM要素と異なり、React要素は単純なオブジェクトになり、簡単に作成されます。React DOMがReactエレメントを把握してそれに従いDOMを更新する作業を担当することになります。 補足: 要素のことを、より広く知られている概念である “コンポーネント” と混同する人もいるかもしれません。コンポーネントについては次の章で説明します。要素とはコンポーネントを “構成する” ものです。次に進む前にこの章を読んでいくことをお勧めします。 要素を DOM としてレンダーする HTMLファイルの中に<div>要素があったとしましょう。 最初の記事で説明したようにindex.htmlにはrootの要素が一つだけありましたね。 ここにReactDOMがすべてのReactのアプリケーションを管理することになるのでルートのDOMノードと呼ぶことにしましょう。 Reactだけで構築されたアプリケーションはDOMノードを一つだけ持ちます。既存のアプリにReactを組み合わせて使いたい場合は独立したDOMノードを複数使用することも可能です。 React 要素をルート DOM ノードにレンダーするには、まず ReactDOM.createRoot() に DOM 要素を渡し、root.render() に React 要素を渡します。 このコードにより”Hello World”が表示されます。 レンダーされた要素の更新 react要素はイミュータブルです。※イミューン(Immune:変更できない) 一度作成された要素の子要素や、属性などを変更することはできません。 今の学習段階でUIを更新する方法は新し要素を作成してroot.render()に渡すという事になります。 以下の例で秒刻みに動く時計の例についてみてみます。 実際の例を見てください。 この例ではsetInterval() のコールバックから root.render() を毎秒呼び出しています。 補足: 上記のコードは例として記載したものであり、実際にはroot.render()を呼び出すことは1度のみ行う事が通常になります。次の章では、上記のコードをstate付きのコンポーネントへとカプセル化する方法を学びます。飛ばさないようにしましょう。 React は必要な箇所のみを更新する ReactDomは要素とその子要素を以前のものと比較しています。その比較の際に差があった場合、必要な部分のみのDOMの更新を行うことになります。 下記の例では時間の変わる要素の部分のみ更新されていることが分かりますね。 このように必要な部分だけリアクティブに要素が変わることでユーザーにインタラクティブなインターフェイスを提供することができますね。

React講座 JSXの使い方

React JSXの使い方

ではReactの仕組みを理解したところでReactのテンプレートシンタックスのJSXを理解していきましょう。 JSXはこのように書くことができます。ストリングでもないのでクオートで囲う必要もありません。 JSXを使わない方法もありますが、Reactを使う上ではJSXを是非使っていきたいです。 JSXはReactのエレメントを作成して、そこからDOMに変換されるようになります。 JSXを使う理由 Reactはイベントへのリスポンスや状態の変化を感知してユーザー側に表示するデータをリアクティブに作成することができます。 Reactの強みでもあるのがこのロジックとマークアップ(HTML)を同じファイルに記述できることです。ファイルが別々でないので一目で見ただけで何がユーザー側に表示されるか理解しやすいです。 この概念をコンポーネントと呼びます。 JSXに式を埋め込む では、次の例を見てみましょう。 nameという変数を宣言し、中括弧{}に入れることでJSX内で使用することができます。 この方法を使うことで、計算式の2+2やオブジェクト(例:user.name)を使うことができます。 JSXは長く記載する場合もあるので()で囲むようにするとよいです。 JSX自体を式として使う 先ほどはJSXに変数を入れる方法を紹介しました。 さらに、JSX自体も式としてif文やforループの文で使用することができます。 ReactがJSXを読み込む際にコンパイルされ普通のJavaSxriptに変換されるようになります。 JSXで属性を指定する 文字列リテラルを属性として指定するために引用符(クオーテーション)を使用できます。 これでHTMLの属性(attribute)にストリングの値を入れてあげることができます。 また、属性に JavaScript 式を埋め込むために中括弧を使用することもできます。これでダイナミックなデータを流し込むことができますね。 注意しておきたいこと JSXはHTMLよりもJavaScriptに近いものになります。ですのでHTMLの属性にclassを入れたい場合はJavaScriptのclassと干渉することを防ぐためにclassNameを使うことになります。 JSXで子要素を指定 タグが空の場合は、XMLのように/>でタグを閉じることができます。 もちろんJSXのタグに子要素(HTMLのタグ、エレメント)を入れることができます。 JSX はインジェクション攻撃を防ぐ JSX にユーザの入力を埋め込むことは安全です: React DOMはJSXに埋め込まれた値をレンダー前にエスケープします。 →エスケープとは、HTML上で特殊文字を期待通りに表示するために施す処理のことです。 →特殊文字に指定されている文字は、割り当てられている記号を記述することで表示できます。 →例えば、<は<であったり、©は©など、特殊文字には必ず該当する記号が割り当たっています。 このため、XSS攻撃の防止になります。 JSXはオブジェクトの表現になる ReactがJSXをコンパイルする流れを見てみましょう。 下記の二つの例は同じものになります。 ①JSXの例 ②JSXを使わない例 上記のコードをReactのcreateElementメソッドで下記の様なオブジェクトを作成することになります。 このようなオブジェクトはReact要素(エレメント)と呼びます。Reactがこれらのオブジェクトを読み取り、必要に応じてDOMを構築し常にリアクティブなデータをユーザーに’届けるようになります。 では、次回は要素のレンダーについて学んでいきましょう。

ViteでReactを始めよう!

Viteでreactを始めよう

皆さん、こんにちは。

今日はViteというコマンドツールでReactのアプリをセットアップしていきます。

今日の条件

Node.js バージョン16 (コマンド node -v)

npm バージョン8(npm -v)

今までのやり方

通常のReactのセットアップは下記のコマンドです。

npx create-react-app my-app

これで自動的にBabelとWebPackがインストールされたreactのプロジェクトが作成されます。

これらのパッケージが古いブラウザにも対応したJavaScriptファイルなどに変換してくれるのですが、その為にファイルが大きくなる+遅くなることがあります。

なので今日使うViteでサクサクの一般的なブラウザのみに対応したセットアップの仕方で設定する方法をお勧めします。

Viteとは

ViteはもともとVueJSの創設者が作成したビルドツールになります。Vueのプロジェクト以外にも対応しており、reactのデベロッパーにも人気です。

ViteでVueのプロジェクトを始めたい場合は下記の記事からどうぞ。

ViteでReactを始める(Yarn)

yarn create vite

#プロジェクト名を聞かれたら
my-vite-app

#フレームワークを聞かれたら
reactを選択

cd プロジェクトフォルダ
yarn 
yarn dev

ViteでReactを始める

ではコマンドラインに下記を叩き込みます。

ファイルパスが自分がプロジェクトを作成したいパスにいることを確認してください。

npm create vite@latest

y(いえす)でエンターを押す。そのままエンターでもOKです。

プロジェクト名を聞かれるので適当に入力します。

画像っだとmovie-appにしてみました。

では使いたいフレームワークを選択するのでここで、reactを選択してエンター

次にJavaScriptを使うのかTypeScriptを使うのか聞かれます。

今回はJavaScriptにしてエンター。

ではreactのベースとなるアプリができたのでフォルダの中に移動してパッケージをインストールして、起動してみます。

表示された通りに順番に入力しましょう。

npm installはnpm iでも同じことができます。

npm run dev

ここで開発用のアプリが起動したのでブラウザから見てみましょう。

カウントのところをクリックすると数が増えていきますね。

へえ。

お疲れ様でした。

VueでPiniaを使ってみよう

VueではState Management SystemのVuexが公式のプラグインとして紹介されてきました。しかし、最新のVue3ではPiniaを使うようにとVue生みの親のEvan Youさんもお勧めしています。

では、State Management(状態管理)って何でしょうか?

State Management(状態管理)

State Managementとはいわばストレージ/ストア(倉庫)のことです。アプリケーションでストアを作っておいてそこにデータを保管できるようになります。

例えばユーザーがログインしたときのトークン、APIでフェッチしたデータ、アプリケーションの状態(例:フォームが提出したとかの状態)があげられます。

PiniaのAPIの使い方

PiniaはVue2でもVue3でも使う事ができます。また、Options API(一般的に初心者向け)でもCompotion APIの書き方でもどちらでも対応しています。私の個人的な意見ではVue3でComspostion APIで書く方法が一番良いと思います。

なぜPiniaなのか

Piniaを使う事でこの状態管理システム(Store Library)を各コンポーネントやページのどこでも使う事ができます。

もちろん、同じことが export const state = reactive({}) でもできますよね。

しかし、このやり方だとセキュリティに脆弱性があり、何を管理しているのか見られてしまう可能性があります。

この他にもデベロッパー用のツールがあったり、サーバー側でもレンダーにも対応することができるなど色々メリットがあります。

Piniaをインストール

npmかyarnのコマンドでインストールしましょう。

yarn add pinia
# or with npm
npm install pinia

※NuxtJSの場合はこちらから

インストールが完了したらmain.jsにPiniaを追加します。

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia)
app.mount('#app')

Piniaのファイルを作成しよう

ではPiniaが追加できたらJSファイルでストアしたいデータを保管できる場所を作っていきましょう。

慣習的にsrcディレクトリの直下にstoreというフォルダを作成してそこにJavaScriptファイルで下記のコードを作るのが一般的になります。

import { defineStore } from 'pinia'

export const useCounterStore = defineStore({
  id: 'counter',
  state: () => ({
    counter: 0
  }),
  getters: {
    doubleCount: (state) => state.counter * 2
  },
  actions: {
    increment() {
      this.counter++
    }
  }
})
  • stateは保管するデータの初期値をリターンするファンクションです。(dataと同じ概念)
  • getters は stateを使ってデータをモディファイ(変更)したいときに使うファンクションです。(copmputedと同じ概念)
  • actionsはasyncにできるファンクションのことです。(methodと同じ概念)

<script setup>的なPiniaの使い方

他のサイトでは上記のやり方でPiniaを使っていますが、今年からVueで使えるようになった<script setup>を使ったやり方に合わせてPiniaも書きたい人は下のやり方をお勧めします。

import { defineStore } from 'pinia'
import { ref } from 'vue';
import EventService from "@/plugins/EventService";

export const useMemberStore = defineStore('member', ()=> {

  const data = ref(null)

  const getData = () => {
    EventService.getMember()
      .then((response) => {
        data.value = response.data;
      })
      .catch((error) => {
        console.log("data:" + error);
      });
  }

  return {
    data,
    getData
  }
})

上のコードを見ても分かるようにgetterとかactionsとかの概念はなく普通のJavaScriptのコードで<script setup>と同じようにVanilla JavaScriptに近い状態で書くこともできます。

個人的にはこちらの方が書きやすいと思ったので是非試してみてください。

ではこれをコンポーネントから読み込めるようにしましょう。

<script setup>

import { useOfficeStore } from "@/stores/members/office";

const officePinia = useOfficeStore();

const officeData = officePinia.data
</script>

このようにどのコンポーネントからでもStoreにアクセスでき、グローバルにデータを管理することで後から見やすくなりますね。

Viteで@/componentsのショートカットが使えない問題

ViteでVueのアプリを作成してコンポーネントをインポートしようとすると何か気づいたことはありませんか?

そうです。Vue CLIで作った際に使えていた@/~のショートカットが使えないです!

なので、毎回コンポーネントをインポートする際にこのように、かなり面倒になってきます。

import Component from '../../../../components/Component.vue'

//本来ならこうしたい。。。
import Component from '@/components/Component.vue'

Viteで@を使えるようにする方法

これはWebPackでついてきたショートカットを再現することで同じように@が使えるようになります。

では、Viteのコンフィグファイルを作成して、コードを書いていきます。

Viteのコンフィグファイルの書き方

Viteのコンフィグはvite.config.jsの名称でプロジェクトのルート(アプリの一番上の階層)にファイルを作成することでできます。

これで、Viteが読み込む際にこのコンフィグファイルも自動で読み込んでくれます。

import { fileURLToPath, URL } from "url";

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
});

あとはいつも通りにコンポーネント側で@を使ってコンポーネントをインポートできるようになります。

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <HelloWorld msg="Hello Vue 3 + Vite" />
</template>

<script>
import HelloWorld from '@/components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

Vue UIライブラリのPrimeVueを使ってみよう

PrimeVueはVue3に対応したUIライブラリで無料で使えます。

公式サイトはこちらです。

この会社名はPrimeFacesといい11名のスタッフで構成されるトルコ(Turkey)の会社です。

会社の利益はテンプレートとライセンス費用で賄っているようです。

PrimeVueの良いところ

コンポーネントの数が半端ではない!!

なんと90+以上のcomponentがバンドルに含まれています。

また、デザインのスッキリしていて洗練されているのでそのまま使っても気持ちが良いです。

PrimeVueをインストールしよう

ではコマンドラインからPrimeVueをインストールします。

VueアプリケーションはViteで作成しました。(なのでWebPackは入ってません。)

#バージョンを指定したい場合
npm install primevue@^3.15.0 --save

#最新のものをインストールする場合
npm install primevue

#コンポーネントで使うアイコンのインストール
npm install primeicons --save

下記のようにpackage.jsonを見ると、インストールしたprimevueとprimeiconsが記載されデペンデンスィー:dependencies(依存されたモジュール)に追加されたことが分かります。

{
  "name": "crm-app",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "primeicons": "^5.0.0",
    "primevue": "^3.15.0",
    "vue": "^3.2.25",
    "vue-router": "^4.1.2"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^2.3.3",
    "vite": "^2.9.9"
  }
}

main.jsにprimeVueを追加

次にmain.jsにPrimeVueを追加してアプリがマウントする際にこのライブラリを読み込ませるようにしましょう。

import { createApp } from 'vue'
import App from './App.vue'
import PrimeVue from 'primevue/config';

const app = createApp(App);

app.use(PrimeVue);
app.mount("#app");

グローバルにコンポーネントを登録

PrimeVueのcomponentをグローバルで使用したい場合は下記のように設定します。

まずは、main.jsに使いたいコンポーネントを登録します。

例で例えるとButtonになります。それからapp.componentメソッドでテンプレートで使いたい名称とインポートするコンポーネント名を指定します。

import { createApp } from 'vue'
import App from './App.vue'
import PrimeVue from 'primevue/config';

import Button from 'primevue/button';

const app = createApp(App);

app.use(PrimeVue);
app.component('Button', Button );
app.mount("#app");

この後に例えばApp.vueで下記のように<Button/>をテンプレートに記載するだけで使えるようになります。

<template>
 <Button label="Disabled" disabled="disabled" />
  <Button label="Submit" icon="pi pi-check" iconPos="right" />
</template>

コンポーネントにインポートする場合

では、グローバルではなく、それぞれのcomponentにPrimeVueのコンポーネントをインポートしたい場合は下記のようにします。

例でいうと、子コンポーネントのHwlloWorld.vueに使いたいPrimeVueのコンポーネントをインポートします。

<script setup>
import Calendar from 'primevue/calendar';
</script>

<template>
  <Calendar v-model="value" />
</template>

CSSをインポートする

ここで気が付いたかもしれませんが、スタイルが入っていませんね。

では、CSS、アイコン、それとテーマをインポートしましょう。

これをmain.jsに追加してください。

import 'primevue/resources/primevue.min.css'
import 'primeicons/primeicons.css'
import 'primevue/resources/themes/luna-blue/theme.css'

ちなみにテーマはluna-blueでインポートしてますが実際には下記のように無料で使えるテーマがあるので色々試してみてください。

primevue/resources/themes/bootstrap4-light-blue/theme.css
primevue/resources/themes/bootstrap4-light-purple/theme.css
primevue/resources/themes/bootstrap4-dark-blue/theme.css
primevue/resources/themes/bootstrap4-dark-purple/theme.css
primevue/resources/themes/md-light-indigo/theme.css
primevue/resources/themes/md-light-deeppurple/theme.css
primevue/resources/themes/md-dark-indigo/theme.css
primevue/resources/themes/md-dark-deeppurple/theme.css
primevue/resources/themes/mdc-light-indigo/theme.css
primevue/resources/themes/mdc-light-deeppurple/theme.css
primevue/resources/themes/mdc-dark-indigo/theme.css
primevue/resources/themes/mdc-dark-deeppurple/theme.css
primevue/resources/themes/tailwind-light/theme.css
primevue/resources/themes/fluent-light/theme.css
primevue/resources/themes/lara-light-indigo/theme.css
primevue/resources/themes/lara-dark-indigo/theme.css
primevue/resources/themes/lara-light-purple/theme.css
primevue/resources/themes/lara-dark-purple/theme.css
primevue/resources/themes/lara-light-blue/theme.css
primevue/resources/themes/lara-dark-blue/theme.css
primevue/resources/themes/lara-light-teal/theme.css
primevue/resources/themes/lara-dark-teal/theme.css
primevue/resources/themes/saga-blue/theme.css
primevue/resources/themes/saga-green/theme.css
primevue/resources/themes/saga-orange/theme.css
primevue/resources/themes/saga-purple/theme.css
primevue/resources/themes/vela-blue/theme.css
primevue/resources/themes/vela-green/theme.css
primevue/resources/themes/vela-orange/theme.css
primevue/resources/themes/vela-purple/theme.css
primevue/resources/themes/arya-blue/theme.css
primevue/resources/themes/arya-green/theme.css
primevue/resources/themes/arya-orange/theme.css
primevue/resources/themes/arya-purple/theme.css
primevue/resources/themes/nova/theme.css
primevue/resources/themes/nova-alt/theme.css
primevue/resources/themes/nova-accent/theme.css
primevue/resources/themes/nova-vue/theme.css
primevue/resources/themes/luna-amber/theme.css
primevue/resources/themes/luna-blue/theme.css
primevue/resources/themes/luna-green/theme.css
primevue/resources/themes/luna-pink/theme.css
primevue/resources/themes/rhea/theme.css

これで下記のようにスタイルが追加されましたね。

では、半端ない数のコンポーネントを楽しんでください。

今日使ったコードはGitHubにあるので見てください。

[Vue入門] asyncコンポーネント

基本的な使い方

大規模なアプリケーションでは、アプリを小さなチャンクに分割し、必要なときにのみサーバーからコンポーネントを読み込む必要があるかもしれません。これを実現するために、Vue には defineAsyncComponent 関数があります:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...サーバーからコンポーネントを読み込む
    resolve(/* 読み込まれたコンポーネント */)
  })
})
// ... `AsyncComp` を普通のコンポーネントと同じように使用する

このように、defineAsyncComponent は Promise を返すローダー関数を受け取ります。Promise の resolve コールバックは、コンポーネントの定義をサーバーから取得したときに呼ばれます。読み込みが失敗したことを示すために、reject(reason) を呼ぶこともできます。

ES モジュールの動的インポート も Promise を返すためにほとんどの場合には defineAsyncComponent と合わせて使用します。Vite や webpack などのバンドラーもこの構文をサポートしているため、の Vue SFC をインポートするためにも使用できます。

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

結果的に得られる AsyncComp は、実際にページ上にレンダリングされるときにローダー関数を呼ぶだけのラッパーコンポーネントです。さらに、内側のコンポーネントに任意の props を渡せるため、非同期ラッパーを使用すると、コンポーネントをシームレスに置換するとともに、遅延読み込みも実現できます。

ローディングとエラーの状態

非同期の操作は必然的にローディングとエラーの状態に関係してきます。そのため、defineAsyncComponent() ではこれらの状態のハンドリングを高度なオプションによりサポートしています。

const AsyncComp = defineAsyncComponent({
  // ローダー関数
  loader: () => import('./Foo.vue'),

  // 非同期コンポーネントの読み込み中に使用するコンポーネント
  loadingComponent: LoadingComponent,
  // ローディングコンポーネント表示前の遅延。デフォルト: 200ms。
  delay: 200,

  // 読み込みに失敗した場合に使用するコンポーネント
  errorComponent: ErrorComponent,
  // エラーコンポーネントは timeout が与えられて
  // その時間を超えた場合に表示される。デフォルト: Infinity。  
  timeout: 3000
})

ローディングコンポーネントが与えられた場合、内側のコンポーネントが読み込まれている間に表示されます。ローディングコンポーネントが表示されるまでに、デフォルトで 200ms の遅延があります。このようになっているのは、高速なネットワークではローディング状態が短く、置き換えが速すぎて、ちらつきのように見えてしまう恐れがあるためです。

エラーコンポーネントが与えられた場合、ローダー関数から返された Promise が reject されたときに表示されます。リクエストが長すぎる場合にエラーコンポーネントを表示するために、timeout を指定することもできます。

Suspense とともに使用する

非同期コンポーネントは、ビルトインコンポーネント <Suspense> とともに使用することもできます。

Vue Routerをインストールしよう

Vue RouterはVueアプリケーションで複数のURLをアプリケーションに導入してView(ページ)を作成する場合に使います。

Vue Cliからインストールする場合

Vue CLI(コマンドラインインターフェイス)を使う場合はアプリを作成するときにVue Routerをインストールするにチェックをするだけで勝手にインストールしてくれます。

npmでインストールする場合

npm install vue-router@4

yarnでインストールする場合

yarn add vue-router@4

main.jsにRouterを追加

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

const app = createApp(App);

app.use(router);
app.mount("#app");

Routerディレクトリを作成

routerフォルダーをsrcフォルダの直下に作成します。その中にindex.jsを作成してrouterのコンフィグレーションを書いていきます。ファイル名はindex.jsでなくてもOKです。

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]
const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})
export default router

viewsディレクトリを作成

では、次に同じようにsrcディレクトリ直下にviewsをいう名前でフォルダーを作成しましょう。ここにページの枠組みとなるVueコンポーネントを作成していきます。

例でいうとAbout.vueとHome.vueになります。これでラウティングの設定が完了しました。あとはメニューバーなどを設置してユーザーを誘導させるようにすればよいですね。

router-linkとrouter-viewを設置

ではApp.vueに下記のようにコードを書き、ラウティングで指定したHome
とAboutにユーザーがアクセスできるようにしましょう。

その行先をクリックした際にViewを表示させるのが<router-view/>になります。

<template>
  <div id="nav">
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link>
  </div>
  <router-view/>
</template>

はい、上のようにメニューバーができて、クリックに応じてView(ページ)が変わりましたね。

これが基本的な使い方になります。

[Vue入門]Provide / Inject

Prop の過剰的な使用(Prop Drilling)

通常、親コンポーネントから子コンポーネントにデータを渡す必要がある場合、props を使用します。しかし、大きなコンポーネントツリーがあり、深くネスト化されたコンポーネントが遠い場合に親元から複数先のコンポーネントにデータを飛ばしたいシナリオを想定してください。

propsのやり方だと、親コンポーネントからつながるすべてのcomponentに同じ prop を渡さなければなりません:

<Footer> コンポーネントは親から受け取るの props を全く使わない場合でもデータの渡し役として、コンポーネントを記載する必要があります。それから<DeepChild> が<Footer>コンポーネントからこのデータにアクセスできるようにしてやっと<Root>コンポーネントのデータを受け取ることができるようになります。これでは、コードが煩雑になり、デバッグの作業も面倒になりますね。

このPropsを何階層も下に投げる作業を省くには provide と inject を使うことで解決できます。親コンポーネントは、そのすべての子コンポーネントに対して 依存関係を提供するプロバイダー (dependency provider) として機能することができます。子ツリー内のどのコンポーネントも、その深さに関係なく、親チェーン内の上位コンポーネントが提供する依存性を注入 (inject) することができます。

Provide

親コンポーネントから子コンポーネントにデータを提供するには provide() 関数を使います:

<script setup>
import { provide } from 'vue'

provide(/* key */ 'message', /* value */ 'hello!')
</script>

<script setup> を使わない場合、setup() 内で provide() が同期的に呼び出されていることを確認してください:

import { provide } from 'vue'

export default {
  setup() {
    provide(/* key */ 'message', /* value */ 'hello!')
  }
}

provide() 関数は 2 つの引数を受け付けます。第 1 引数はインジェクションキーと呼ばれ、文字列または Symbol となります。provideで使用するデータを投げるために使うニックネームのようなものですね。

このインジェクションキーは、子のコンポーネントが、インジェクション(注入)に必要な値を探すのに使われます。1 つのコンポーネントが異なる値を提供するために、異なるインジェクションキーで provide() を複数回呼び出すことができます。

第 2 引数は提供される値です。この値は refs のようなリアクティブな状態を含む、任意の型にすることができます:

import { ref, provide } from 'vue'

const count = ref(0)
provide('key', count)

リアクティブな値を提供することで、提供された値を使用する子孫コンポーネントが、プロバイダーコンポーネントとのリアクティブな接続を確立することができます。

アプリケーションレベルの Provide

コンポーネント内だけでなく、アプリケーションレベルでデータを提供することも可能です:

import { createApp } from 'vue'

const app = createApp({})

app.provide(/* key */ 'message', /* value */ 'hello!')

アプリケーションレベルの Provide は、アプリケーションでレンダリングされるすべてのコンポーネントで利用可能です。これは特にプラグインを書くときに便利です。プラグインは通常、コンポーネントを使ってデータを提供することができないからです。

Inject

親コンポーネントが提供するデータを注入するには inject() 関数を使用します:

<script setup>
import { inject } from 'vue'

const message = inject('message')
</script>

提供された値が ref である場合、そのまま注入され、自動的にアンラップされることはありません。これにより、インジェクターコンポーネントはプロバイダーコンポーネントとのリアクティビティの接続を保持することができます。

繰り返しますが、もし <script setup> を使用しないのであれば、inject() は setup() の内部でのみ同期的に呼び出す必要があります:

import { inject } from 'vue'

export default {
  setup() {
    const message = inject('message')
    return { message }
  }
}

インジェクションのデフォルト値

デフォルトでは、inject は注入されるキーが親チェーンのどこかで提供されることを想定しています。キーが提供されていない場合、実行時が出ます。

インジェクトされたプロパティをオプションのプロバイダーで動作させたい場合は、props と同様にデフォルト値を宣言する必要があります:

// もし "message" にマッチするデータがなかった場合は、
// `value` は "default value" になります
const value = inject('message', 'default value')

場合によっては、関数を呼び出したり、新しいクラスをインスタンス化したりして、デフォルト値を作成する必要があるかもしれません。オプションの値が使用されないケースで不要な計算や副作用を避けるために、デフォルト値を作成するためのファクトリー関数を使用することができます:

const value = inject('key', () => new ExpensiveClass())

リアクティビティと共に利用する

リアクティブな値を provide / inject する場合、可能な限り、リアクティブな状態への変更を provider の内部で維持することが推奨されます。これは、提供されるステートとその可能な変更が同じコンポーネントに配置されることを保証し、将来のメンテナンスをより容易にするためです。

インジェクターコンポーネントからデータを更新する必要がある場合があります。そのような場合は、状態の変更を担当する関数を使うことをおすすめします:

<!-- プロバイダーコンポーネント内部 -->
<script setup>
import { provide, ref } from 'vue'

const location = ref('North Pole')

function updateLocation() {
  location.value = 'South Pole'
}

provide('location', {
  location,
  updateLocation
})
</script>
<!-- インジェクターコンポーネント内部 -->
<script setup>
import { inject } from 'vue'

const { location, updateLocation } = inject('location')
</script>

<template>
  <button @click="updateLocation">{{ location }}</button>
</template>

最後に、provide を通して渡されたデータが注入されたコンポーネントによって変更されないようにしたい場合は、提供された値を readonly() でラップすることができます。

<script setup>
import { ref, provide, readonly } from 'vue'

const count = ref(0)
provide('read-only-count', readonly(count))
</script>

シンボルキーと共に利用する

今までの例では、文字列のインジェクションキーを使っていました。もしあなたが多くの依存関係を提供するプロバイダーを持つ大規模なアプリケーションで作業していたり、他の開発者が使用する予定のコンポーネントを作成している場合は、名前の重複を避けるためにシンボルインジェクションキーを使用するのがベストです。

シンボルは専用のファイルに書き出しておくことをおすすめします:

// keys.js
export const myInjectionKey = Symbol()
// プロバイダーコンポーネント内
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'

provide(myInjectionKey, {
  /* 提供するデータ */
})
// インジェクターコンポーネント内
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'

const injected = inject(myInjectionKey)

[Vue入門] スロット

このページは、コンポーネントの基本をすでに読んでいることを前提としています。コンポーネントを初めて使用する場合は、最初にそれをお読みください。

スロットコンテンツとその出力先

コンポーネントはpropsを受け入れることができることを学びましたね。propは色々なタイプのJavaScriptのデータを格納することができます。しかし、テンプレートコンテンツはどうでしょうか?再利用可能なコンポーネントを作成するうえで、親のコンポーネントから自在に意図した場所にデータをレンダーしたい場合にスロットが役に立ちます。

たとえば、次の<FancyButton>ような使用法をサポートするコンポーネントがある場合があるとします。

<FancyButton>
  Click me! <!-- slot content -->
</FancyButton>

のテンプレートは<FancyButton>次のようになります。

<button class="fancy-btn">
  <slot></slot> <!-- slot outlet -->
</button>

この<slot>エレメントは、親が提供するスロットコンテンツをレンダリングする場所を示すスロットアウトレットです。

いわば枠組みを作ってあげてその中(スロット)に意図したデータを投げてあげるわけですね。

そして、最終的にレンダリングされたDOMはこのようになります。:

<button class="fancy-btn">
  Click me!
</button>

スロットを使用すると、<FancyButton>は外側(およびその派手なスタイリング)のレンダリングを担当し、<button>内側のコンテンツは親コンポーネントによって提供されます。

スロットを理解するもう1つの方法として、スロットをJavaScript関数と比較することができます。

// parent component passing slot content
FancyButton('Click me!')

// FancyButton renders slot content in its own template
function FancyButton(slotContent) {
  return (
    `<button class="fancy-btn">
      ${slotContent}
    </button>`
  )
}

スロットコンテンツはテキストだけに限定されません。有効なテンプレートコンテンツであれば何でもOKです。たとえば、複数のHTMLエレメント、または他のコンポーネントを渡すことができます。

<FancyButton>
  <span style="color:red">Click me!</span>
  <AwesomeIcon name="plus" />
</FancyButton>

スロットを使用することで、<FancyButton>をより柔軟で再利用可能なコンポーネントとして使用できるようになります。

Vueコンポーネントのスロットメカニズムは、ネイティブのWebコンポーネント<slot>要素で記載されていますが、後で説明する他の機能も備えています。

レンダリングスコープ

スロットコンテンツは、親で定義されているため、親コンポーネントのデータスコープにアクセスできます。例えば:

<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

ここでは、両方の{{ message }}補間で同じコンテンツがレンダリングされます。

スロットコンテンツは、子コンポーネントのデータにアクセスできません。原則として、次の点に注意してください。

親テンプレートのすべてが親スコープでコンパイルされ、子テンプレートのすべてが子スコープでコンパイルされます。

スロット内容が指定されていない場合の初期値

コンテンツが提供されていない場合にのみレンダリングされるように、スロットのフォールバック(つまりデフォルト)コンテンツを指定できるという便利な機能があります。たとえば、<SubmitButton>コンポーネントでは次のようになります。

<button type="submit">
  <slot></slot>
</button>

<button>親コンポーネントでスロットコンテンツを提供しなかった場合は、「Submit」というテキストをの中に表示したい場合を想定します。<slot>タグの間にフォールバックコンテンツ(スロット内容が指定されていない場合の初期値)を記載することで初期値を「Submit」にできます。

<button type="submit">
  <slot>
    Submit <!-- fallback content -->
  </slot>
</button>

親コンポーネントで使用する場合<SubmitButton>、スロットにコンテンツを提供なかった場合。

<SubmitButton />

これにより、フォールバックコンテンツ(デフォルト値)「Submit」がレンダリングされます。

<button type="submit">Submit</button>

ただし、スロットコンテンツを提供する場合:

<SubmitButton>Save</SubmitButton>

次に、提供されたコンテンツが代わりにレンダリングされます。

<button type="submit">Save</button>

複数のスロットがある場合

1つのコンポーネントに複数のスロットがあると便利な場合があります。次のテンプレート<BaseLayout>を使用するコンポーネントでは、次のようになります。

<div class="container">
  <header>
    <!-- We want header content here -->
  </header>
  <main>
    <!-- We want main content here -->
  </main>
  <footer>
    <!-- We want footer content here -->
  </footer>
</div>

このような場合、<slot>エレメントには特別な属性として、nameというものがあります。これを使用して、コンテンツをレンダリングする場所を指定できるようになり、さまざまなスロットに意図したIDを割り当てることができます。

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

例を見てわかるようにスロットネームのないデフォルト用のの<slot>があります。name slotを使用する親コンポーネント<BaseLayout>では、それぞれが異なるスロット出力先をターゲットとする複数のスロット出力先に提供する方法が必要です。ここで名前付きスロットの出番です。

名前付きスロットを渡すには、ディレクティブで<template>要素を使用してから、v-slotを使ってスロットの名前を引数として次のように渡す必要があります。

<BaseLayout>
  <template v-slot:header>
    <!-- content for the header slot -->
  </template>
</BaseLayout>

v-slotは省略して#記載できるので、<template v-slot:header>から<template #header>と記載することができます。

3つのスロットすべてのコンテンツを<BaseLayout>省略構文を使用して渡す場合は次のとおりになります。

<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <template #default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

コンポーネントがデフォルトスロットと名前付きスロットの両方を受け入れる場合、すべての上階層の<template>ではないノード(HTML)は自動的にデフォルトスロットのコンテンツとして扱われます。したがって、上記は次のように書くこともできます。

<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <!-- implicit default slot -->
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

これで、エレメント内のすべて<template>が対応するスロットに渡されます。最終的にレンダリングされるHTMLは次のようになります。

<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

繰り返しになりますが、JavaScript関数の例を使用すると、名前付きスロットをよりよく理解するのに役立つ場合と思います。

// passing multiple slot fragments with different names
BaseLayout({
  header: `...`,
  default: `...`,
  footer: `...`
})

// <BaseLayout> renders them in different places
function BaseLayout(slots) {
  return (
    `<div class="container">
      <header>${slots.header}</header>
      <main>${slots.default}</main>
      <footer>${slots.footer}</footer>
    </div>`
  )
}

ダイナミックなスロット名

動的ディレクティブ引数はv-slotでも使用可能で、動的スロット名の定義をすることができます。

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- with shorthand -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

式は動的ディレクティブ引数の構文制約に従うことに注意してください。

スコープスロット

レンダリングスコープで説明したように、スロットコンテンツは子コンポーネントの状態にアクセスできません。

ただし、スロットのコンテンツが親スコープと子スコープの両方のデータを利用できると便利な場合があります。これを実現するには、子がデータをレンダリングするときにスロットにデータを渡す方法が必要になります。

これは、Propsをコンポーネントに渡すのと同じように、属性をスロットアウトレットに渡すことがで実現可能になります。:

<!-- <MyComponent> template -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

単一のデフォルトスロットを使用する場合と名前付きスロットを使用する場合では、スロットプロップの受信の仕方が少し異なります。v-slot子コンポーネントタグを直接使用して、最初に単一のデフォルトスロットを使用してPropsを受け取る方法を記載します。

<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

子によってスロットに渡されるpropsは、対応するv-slotディレクティブの値として使用でき、スロット内の式からアクセスできます。

スコープスロットは、子コンポーネントに渡される関数と考えることができます。次に、子コンポーネントはそれを呼び出し、Propsを引数として渡します。

MyComponent({
  // passing the default slot, but as a function
  default: (slotProps) => {
    return `${slotProps.text} ${slotProps.count}`
  }
})

function MyComponent(slots) {
  const greetingMessage = 'hello'
  return (
    `<div>${
      // call the slot function with props!
      slots.default({ text: greetingMessage, count: 1 })
    }</div>`
  )
}

実際、これはスコープ付きスロットのコンパイル方法、および手動レンダリング関数でスコープ付きスロットを使用する方法に非常に近いものです。

スロット機能のシグネチャとv-slot=”slotProps”がどのように一致するかに注意してください。関数の引数と同じように、v-slot内で次の場所で各データを使用できます。

<MyComponent v-slot="{ text, count }">
  {{ text }} {{ count }}
</MyComponent>

名前付きスコープスロット

名前付きスコープスロットも同様に機能します。スロットのPropsは、v-slotディレクティブの値としてアクセスできますv-slot:name="slotProps"。省略した記載方法を使用すると、次のようになります。

<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>

  <template #default="defaultProps">
    {{ defaultProps }}
  </template>

  <template #footer="footerProps">
    {{ footerProps }}
  </template>
</MyComponent>

名前付きスロットにPropsを渡す:

<slot name="header" message="hello"></slot>

nameスロットでnameというストリングが使用されるため、nameというプロップは制限され、Propsには含まれないことに注意してください。そのため、結果headerPropsは{ message: ‘hello’ }になります。

ファンシーリストの例

スコープ付きスロットの良い使い方は何か疑問に思われるかもしれません。次に例を記載します。<FancyList>アイテムのリストをレンダリングするコンポーネントを想像してみてください。リモートデータの読み込み、データを使用したリストの表示、さらにはページネーションや無限スクロールなどの高度な機能のロジックをカプセル化できます。ただし、各アイテムの外観に柔軟性を持たせ、各アイテムのスタイルをそれを使用する親コンポーネントに任せる必要があります。したがって、望ましい使用法は次のようになります。

<FancyList :api-url="url" :per-page="10">
  <template #item="{ body, username, likes }">
    <div class="item">
      <p>{{ body }}</p>
      <p>by {{ username }} | {{ likes }} likes</p>
    </div>
  </template>
</FancyList>

<FancyList>内部では、異なるアイテムデータを使用して同じものを複数回レンダリングできます(オブジェクトをスロットプロップとして渡すために<slot>v-bindを使用していることに注意してください)。

<ul>
  <li v-for="item in items">
    <slot name="item" v-bind="item"></slot>
  </li>
</ul>

レンダーレスコンポーネント

上で説明した<FancyList>ユースケースは、再利用可能なロジック(データのフェッチ、ページネーションなど)とビジュアル出力の両方をカプセル化し、スコープ付きスロットを介してビジュアル出力の一部をコンシューマーコンポーネントとして使用したものになります。

この概念をもう少しひねると、ロジックのみをカプセル化し、それ自体では何もレンダリングしないコンポーネントを作ることができます。ビジュアル出力は、スコープスロットを備えたコンシューマーコンポーネントに完全に任されることになります。このタイプのコンポーネントをレンダーレスコンポーネントと呼びます。

レンダーレスコンポーネントの例としては、現在のマウス位置を追跡するロジックをカプセル化したものがあります。

<MouseTracker v-slot="{ x, y }">
  Mouse is at: {{ x }}, {{ y }}
</MouseTracker>

興味深いパターンですが、レンダリングレスコンポーネントで実現できることのほとんどは、余分なコンポーネントのネストのオーバーヘッドを発生させることなく、CompositionAPIを使用してより効率的な方法で実現できます。後で、Composableと同じマウス追跡機能を実装する方法を説明します。

まとめとしてスコープスロットは、<FancyList>の例のように、ロジックの格納化とビジュアル出力の作成の両方が必要な場合で役立つことが分かりますね。