kurosame’s diary

フロントエンド中心です

React DnD v11(古いバージョン)の実装

はじめに

少し癖のあるReact DnDを古いバージョンで使わなければならない案件を対応しました
3 つくらい古いメジャーバージョンだったので、公式ドキュメントの Examples が動かず、かつ、ネット上にドキュメントが少なかったので、一応実装をこの記事に残しておきます

この記事の React DnD のバージョンは以下です

"react-dnd": "11.1.3",
"react-dnd-html5-backend": "11.1.3",

ちなみに現在の最新は v14 です
v11 でも hooks に対応されていた分、まだ楽な方だったかなと思います

実装内容は以下の Example のような D&D(ドラッグ&ドロップ)による要素の入れ替えです
https://react-dnd.github.io/react-dnd/examples/sortable/simple

Example では要素をいい感じに並び替えていますが、今回は実装を簡単にするため swap(入れ替え)するだけにしました

ルートコンポーネントの設定

ルートコンポーネントで以下を設定

import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";

<DndProvider backend={HTML5Backend}>
  <Example />
</DndProvider>;

コンポーネントの実装

コンポーネントでは、State と D&D された時に要素を入れ替えるロジック(moveBox)を持っています
moveBox は fromId(ドラッグしたアイテムの ID)と toId(ドロップされたアイテムの ID)を引数で渡して、場所を入れ替えているだけです

import { useState, useCallback } from "react";
import produce from "immer";

const [items, setItems] = useState([
  { id: 1, text: "アイテム1" },
  { id: 2, text: "アイテム2" },
  { id: 3, text: "アイテム3" },
]);

const findIdx = useCallback(
  (id: number) => items.findIndex((c) => c.id === id),
  [items]
);
const moveBox = useCallback(
  (fromId: number, toId: number) => {
    const from = findIdx(fromId);
    const to = findIdx(toId);
    const swapItems = produce(items, (c) => {
      [c[from], c[to]] = [c[to], c[from]];
      return c;
    });
    setItems(swapItems);
  },
  [items]
);

const DND_TYPE = "items_dnd";

return (
  <>
    {items.map((c) => (
      <BoxDnD key={c.id} id={c.id} type={DND_TYPE} moveBox={moveBox}>
        <Example />
      </BoxDnD>
    ))}
  </>
);

後述する<BoxDnD />が View の部分ですが、要素を入れ替えるロジックは別で切り離しておいた方が良いでしょう
D&D ライブラリを別のものに替えても使えると思うので

コンポーネントの実装

useDrag と useDrop という 2 つの hooks を駆使して実装します

interface BoxDndProps {
  id: number;
  type: string;
  moveBox(fromId: number, toId: number): void;
  children: React.ReactNode;
}

export const BoxDnD = ({ id, type, moveBox, children }: BoxDndProps) => {
  type ItemType = {
    type: typeof type;
    id: number;
  };

  const [{ isDragging }, drag] = useDrag<
    ItemType,
    unknown,
    { isDragging: boolean }
  >({
    item: { type, id },
    collect: (m) => ({ isDragging: m.isDragging() }),
  });
  const [, drop] = useDrop({
    accept: type,
    drop({ id: fromId }: ItemType) {
      if (fromId !== id) moveBox(fromId, id);
    },
  });

  return (
    // Boxの実装は省略
    <Box ref={(node) => drag(drop(node))} isDragging={isDragging}>
      {children}
    </Box>
  );
};

useDrag

v14 だとコールバック関数になってますが、v11 だとただの関数ですね

useDrag のパラメーターの item は識別子となるプロパティです
type はドロップターゲットを特定するために必要で、上記実装では string 型にしていますが Symbol 型の方が良さそうですね
id はアイテム要素を特定するために一意の識別子を設定します
今回はアイテムの ID を渡してます

useDrag のパラメーターの collect はコールバック関数になっていて、ランタイムで実際にドラッグした時、引数の m(monitor)にドラッグの状態が入ってきます
上記の実装では、ドラッグされているかの boolean を返す isDragging を返しています

useDrag の戻り値は 3 つの要素を持つタプルで返ってきます
タプルの 1 つ目は、collect で収集したオブジェクトを受け取ります
collect 関数を定義しなかった場合、空オブジェクトを返します
タプルの 2 つ目は、drag の ref を返します
これをドラッグする DOM にアタッチします
タプルの 3 つ目は、drag の preview の ref を返します
今回は使用しませんが、ドラッグしている時は別の DOM へアタッチしたい場合に使用します

useDrop

こちらも v14 だとコールバック関数になってますが、v11 だとただの関数です

useDrop のパラメーターの accept はドロップターゲットの識別子です

useDrop のパラメーターの drop はドロップされた時に呼び出される関数です
drop の第 1 引数にドラッグされている item を保持しています
上記実装では、ドロップされた時に moveBox を呼び出しています

useDrop の戻り値は 2 つの要素を持つタプルで返ってきます
タプルの 1 つ目は、collect で収集したオブジェクトを受け取ります
今回は使用していません
タプルの 2 つ目は、drop の ref を返します
これをドロップターゲットの DOM にアタッチします

ドラッグされる DOM とドロップされる DOM が同じ場合

今回だと Box コンポーネントですが、以下の書き方でいけます
drag(drop(ref))

[追記 1/24] swapではなく、並び替えにした場合の実装

// 親コンポーネント
const findItem = useCallback(
  (id: number) => {
    const idx = items.findIndex(c => c.id === id);
    return { idx, item: items[idx] };
  },
  [items]
);
const moveBox = useCallback(
  (fromId: number, toId: number) => {
    const { idx: fromIdx, item } = findItem(fromId);
    const { idx: toIdx } = findItem(toId);
    const sortItems = produce(items, c => {
      c.splice(fromIdx, 1);
      c.splice(toIdx, 0, item);
      return c;
    });
    setItems(sortItems);
  },
  [items]
);

// 子コンポーネント
const [, drop] = useDrop({
  accept: DND_TYPE,
  hover({ id: fromId }: ItemType) {
    if (fromId !== id) moveBox(fromId, id);
  },
});

TypeScript のユニオン型とインターセクション型について

はじめに

ユニオン型(|)とインターセクション型(&)の違いを簡単な例でおさらいしておこうと思います

以下の型定義を利用して、実装します

type A = {
  a: string
}

type B = {
  b: number
}

type U = A | B
type I = A & B

すべてのプロパティを実装する

const u: U = {
  a: '',
  b: 0
}

const i: I = {
  a: '',
  b: 0
}

上記は当然ですが、uiのどちらも TS エラーは起きません

存在しないプロパティを実装する

cというABのどちらにも存在しないプロパティを設定する

const u: U = {
  a: '',
  b: 0,
  c: false
}

const i: I = {
  a: '',
  b: 0,
  c: false
}

上記はuiどちらもエラーになる
(そりゃそうだ)

ユニオン型で一部のプロパティを実装する

const u: U = {
  a: ''
}

上記はエラーにならない
ユニオン型はABのプロパティのいずれかを実装していれば良い
よって、Aのプロパティ(a)を実装しているので問題ない

const u: U = {}

上記はエラー
ABのプロパティのいずれかを実装できていない

インターセクション型で一部のプロパティを実装する

const i: I = {
  a: ''
}

上記はエラーになる
インターセクション型はABのプロパティのすべてを実装する必要がある
よって、Bのプロパティ(b)を実装する必要がある

const i: I = {}

上記は当然エラー

Aのプロパティをオプショナル型にしてみる

プロパティをオプショナル型にするとどうなるか

type A = {
  a?: string
}

type B = {
  b: number
}

type U = A | B
type I = A & B
const u: U = {}

上記はエラーにならない
ユニオン型はABのプロパティのいずれかを実装していれば良いので、上記はaの undefined が実装できているということになる

const i: I = {}

上記はエラー
インターセクション型の場合は、ABのプロパティのすべてを実装する必要があるので、この場合はbを必ず実装する必要がある
ちなみにbもオプショナル型にすると、上記はエラーになりません

ここまでのまとめ

ここまでの例をまとめると、ユニオン型がABのプロパティのいずれかを実装で、インターセクション型がABのプロパティのすべてを実装と言えます

今度は以下の型定義を例に実装します

type A = {
  a: string
  b: number
}

type B = {
  b: number
  c: string
}

type U = A | B
type I = A & B

const u: U
const i: I

上記のuiの変数は初期値が設定されていないため、コンパイルエラーになっています
初期値は何を与えればよいでしょうか

u{ a: "a", b: 1 }もしくは、{ b: 1, c: "c" }です(値は適当)
i{ a: "a", b: 1, c: "c"}です(値は適当)

つまり、
uABのプロパティのいずれかを実装(bは必須で実装し、acはいずれかが実装されていないといけない)
iABのプロパティのすべてを実装
となります

また、ui内のプロパティを参照する際も、
ubは参照できるが、acは存在するか確定させるまで参照不可となります
つまり、型ガード(a in u)などを使って、acが存在することを確定させないと、参照できません
ia,b,cのすべてのプロパティが参照できます

もし、上記の初期値を設定した上で、以下のようにbの型を異なるものに変えるとどうなるでしょうか

// bがnumber型
type A = {
  a: string
  b: number
}

// bがstring型
type B = {
  b: string
  c: string
}

この場合、
A | Bの時のbnumber | stringとなり、number型かstring型を設定すれば良い
A & Bの場合は、コンパイルエラー(後述のプリミティブ型データの場合を参照)
となります

プリミティブ型データの場合

最後にプリミティブ型の例です

type U = string | number
type I = string & number
const us: U = '' // ok
const ui: U = 0 // ok
const ub: U = false // エラー

const is: I = '' // エラー
const ii: I = 0 // エラー
const ib: I = false // エラー

ユニオン型は、分かりやすいですね
インターセクション型は、string 型と number 型の両方を表現するのは不可能なので、never 型になります
never 型は事前に起こることを想定してはいけない型なので、never 型の変数を定義し、何かを代入することは不可能です
つまり、プリミティブ型データのインターセクション型というものは定義できないということになります

SvelteKit(β 版)を見てみる

はじめに

Svelte+SvelteKit の環境がコマンド 1 つで構築できるので、その構成を軽く見てみようと思います

Web フレームワークSvelteKitSapperの後継で将来的にこちらに置き換わります

この記事の@sveltejs/kitのバージョンは1.0.0-next.151です
まだ β 版です

セットアップ

npm init svelte@next Project-name

インタラクティブに環境をセットアップできる
以下を選択

  • Which Svelte app template?
    • SvelteKit demo app
  • Use TypeScript?
    • Yes
  • Add ESLint for code linting?
    • Yes
  • Add Prettier for code formatting?
    • Yes

設定周りで気になった点

  • .prettierrcは最初に自分用の設定に変えておく
  • package.json"type": "module"が設定してある
    • これはスコープ内の JS ファイルを ES Modules(import/export)として扱う
    • この設定の影響を受けるのはsvelte.config.jsで、試しに"type": "commonjs"とかにすると、この config が動かない
    • また、.eslintrc.jsも影響を受けてて、こちらは ESM として扱うと動かないので、拡張子を変えて.eslintrc.cjsで CJS 化されている
    • JS ファイルは書かない想定なので、"type": "module"を削除して、svelte.config.mjsにしようかと試したが、node_modules/@sveltejs/kit/dist/cli.jssvelte.config.jsという名前で読み込んでいるので、それもできなかった
  • TypeScript を動かすにはプリプロセッサが必要
    • svelte-preprocessというパッケージで公開されていて、これがsvelte.config.jsで設定されている
  • svelte.config.jskit: { target: '#svelte' }の設定
    • これは SSR された HTML を<div id="svelte">に hydrate する設定、OFF にすることは無さそう
  • TODO デモアプリ(/todos)で追加・削除したデータが永続化されている
  • ビルドが速すぎだった
    • ビルドツールに Vite を使ってる

追加した設定

  • ESLint にeslint-config-airbnb-baseのルールを追加した

コマンド

  • dev(svelte-kit dev
    • .svelte-kit/devにビルド結果が出力される
    • 同時にサーバーを立ち上げる
    • HMR で開発できる
  • build(svelte-kit build
    • 本番用ビルド
    • svelte.config.jsで adapter を指定し、デプロイ環境にあわせてビルド結果を切り替えることができる
    • 特定環境の adapter(adapter-netlifyadapter-vercelなど)もあり、自作することも可能(NPM レジストリに色んな adapter が公開されている)
      • この仕組みはいいなって思った
    • とりあえず@sveltejs/adapter-staticを使うことにしたが、npm i -D @sveltejs/adapter-static@nextにしないとエラーで動かなかった(β 版なのであまり参考にならないが)
      • 結果は/buildディレクトリに出力される
      • ちゃんと静的ビルドされてるっぽかった
  • previewsvelte-kit preview
    • svelte-kit buildの結果を表示できる
    • 本番ビルドの結果をローカルで確認する用
    • 注意点として、/buildではなく、.svelte-kit/outputの方を参照してるので、adapter でビルドされた結果は見ていない
  • check(svelte-check
    • 未使用 CSS、A11y、TS 型チェックを行う
    • Lint やフォーマッターと一緒に CI で走らすと良さそう

ここまでのsvelte.config.jsは以下

import adapter from '@sveltejs/adapter-static'
import preprocess from 'svelte-preprocess'

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: preprocess(),
  kit: {
    adapter: adapter(),
    target: '#svelte'
  }
}

export default config

コード(src配下)

SvelteKit のディレクトリ構成はsvelte.config.jsの files プロパティで定義されており、以下のドキュメントにデフォルト値が書いてある
https://kit.svelte.dev/docs#configuration

  • hooks(src/hooks
    • SSR 時に実行される
    • handle、handleError、getSession、externalFetch という関数が用意されている
    • たとえば、handle 関数はクライアントとサーバーのすべてのリクエストを handle し、処理を挟める
    • ちなみに React Hooks は関係ない
  • lib(src/lib
  • routes(src/routes
    • ルーティング周り、Next.js や Nuxt.js でいう pages ディレクト
    • サーバーのみで動くエンドポイントもページごとに置ける
      • .tsファイルで作成する
      • Node.js 周りの処理を書く
    • ルーティングに含めたくない private なファイルは_を先頭に付ければ良い
    • routes/__layout.svelteは、Nuxt.js にもあるレイアウトコンポーネント
  • serviceWorker(src/service-worker
    • ここに SW を置けば Vite が自動で登録してくれる
  • template(src/app.html
    • テンプレート

このデモアプリのコード色々やってて、勉強になる

この後やること

  • テスト
  • UI コンポーネント入れる
    • なんとなくSmelte良さそう
      • React の Chakra UI みたいな感じにみえる
  • リンター、フォーマッター、svelte-check をどこで走らすか考える
    • npm run devか Git コミットする前か CI か

静的サイトの Next.js を SPA でルーティングさせる

はじめに

この記事の内容は、以下のサイトと同じことをやってます
この記事は自分用のメモです

https://colinhacks.com/essays/building-a-spa-with-nextjs
https://blog.hey3.dev/posts/nextjs-spa

概要

Next.js アプリを静的ファイルにビルドし、S3 にデプロイ、CloudFront で公開していた
しかし、動的ルーティングを使うことになり、これ自体は Next.js がサポートしているので問題ないが、/task/[id].htmlに対して URL から直接アクセスが来た場合に 403 エラーとなってしまう
単純にルートの/index.htmlに飛ばすだけで良いなら、CloudFront のカスタムエラーページの設定で対応できるが、今回はシステムの仕様上、それはできず、/task/[id].htmlの内容を表示させる必要がある

今回やりたいことは以下である

  • システムは静的サイトホスティングする
    • サーバー上でシステムは動かさない
  • ルートの JS でルーティングを実装する
    • Next.js のルーティングを利用しない
  • URL 直接入力の場合、必ずルートの JS を経由させ、ルーティングを通るようにする

Next.js はファイルシステムに沿ったルーティングを持つ(pages ディレクトリにファイルを配置すると自動でルーティングされる)
しかし、サーバー上で動いていない Next.js では動的ルーティング + URL 直接入力に対応できない
こちらはreact-routerで対応する

そうなると、create-react-appで良くて、Next.js 使わなくてもいいんじゃないって感じだけど、バンドルの分割機能やページ単位の SSR など Next.js 機能の恩恵もあるのと、途中で Next.js を使うのやめるのちょっとしんどいので、Next.js の利用を継続する

Nuxt.js であれば、mode: spassr: falseで対応できるのかもしれない

react-routerの実装

npm i react-router-dom

ルートの JS でルーティングを実装する

// pages/index.tsx
import { BrowserRouter, Route, Switch } from 'react-router-dom'

const App: React.VFC = () => (
  <BrowserRouter>
    <Switch>
      <Route path="/sign-in" component={SignIn} exact />
      <Route path="/" component={Home} exact />
      <Route path="/task/:id" component={Task} exact />
    </Switch>
  </BrowserRouter>
)

エラーの対応

ルーティング実装後、サーバー上で動かすと以下のエラーが出る

Error: Invariant failed: Browser history needs a DOM

また、静的ビルドの場合は、以下のエラーになる

Error occurred prerendering page "/". Read more: https://err.sh/next.js/prerender-error

これはreact-routerがグルーバルオブジェクトの window を参照しているが、Node.js 上では window は未定義なので、エラーになっている
よって、react-routerを使うには Node.js のレンダリングを廃止する必要がある

// pages/_app.tsx
const App = ({ Component, pageProps }: AppProps): JSX.Element => (
  <div suppressHydrationWarning={process.env.NODE_ENV === 'development'}>
    {typeof window === 'undefined' ? null : <Component {...pageProps} />}
  </div>
)

window が undefined の場合、null を返して、レンダリングさせないようにしている

また、上記を行ったことでクライアントとサーバーでレンダリング結果が異なるため、以下のエラーが出る

Warning: Expected server HTML to contain a matching <div> in <div>.

これはsuppressHydrationWarningを指定することで警告を消せる

URL 直接入力の対応

今の状態だと URL 直接入力で画面表示を行う場合、react-routerを通さないと 403/404 エラーになる
たとえば、/task/1でリロードすると、Next.js は/task/1.htmlを探しにいくが、これはないので 403/404 エラーを返す

以下の rewrites 設定を行うことで、すべてのパスをルート(ルーティング書いた場所)に書き換えることができる

// next.config.js
module.exports = {
  async rewrites() {
    return [{ source: '/:path*', destination: '/' }]
  }
}

ただし、rewrites はサーバー上でのみ機能するため、静的サイトホスティングしている場合は機能しない

CloudFront の場合の対応としては、カスタムエラーレスポンスの設定で 403 エラーが起きた場合、レスポンスコードを 200 に変更し、レスポンスページをルート(/)にすればいける

さいごに

サーバーレンダリングを抑制したり、next/routerreact-routerが共存したりして、やってることは微妙かもしれないが、Next.js を利用しつつ、動的ルーティング + URL 直接入力に対応するにはおそらく 1 番簡単なやり方だと思う

react-routerを使わないやり方だと、Lambda@Edge による URL 書き換えや静的サイトホスティングをやめて、ECS 等で Next.js を動かすやり方があると思うが、Lambda の場合は複雑化したり、ECS の場合は途中から切り替えるのはしんどかったりするかなと思う

ECS Fargate に exec する

はじめに

トラブルシューティングなどで、ECS Fargate のコンテナーに入ってコマンドを実行する手順です
コンテナーの実行環境が EC2 であれば、普通に SSH でログインして、docker execすればよいのですが、Fargate の場合はこれが使えません

ECS Exec + AWS Systems Manager(SSM)を使うと、Fargate 内のコンテナーでも exec できるようになります

事前準備

  • AWS CLI v2.1.31 以上
  • Fargate プラットフォームのバージョンが 1.3.0 以上

macOS で作業しています

ECS タスクロールの作成

ロールを作成したら、ロールの信頼関係を以下に設定

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

ロールに以下のポリシーをアタッチ

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ssmmessages:CreateControlChannel",
        "ssmmessages:CreateDataChannel",
        "ssmmessages:OpenControlChannel",
        "ssmmessages:OpenDataChannel"
      ],
      "Resource": "*"
    }
  ]
}

ECS のサービスを更新

AWS CLI を使って行う

aws ecs update-service --service <value> --cluster <value> --enable-execute-command

もし、サービスを新規作成する場合は、aws ecs create-serviceコマンドに--enable-execute-commandオプションを付与して実行する

実行中の ECS タスクを停止し、新しいタスクを起動させる(enableExecuteCommand が有効になる)

以下のコマンドで enableExecuteCommand オプションが有効になっているか確認できる

aws ecs describe-tasks --cluster <value> --task <value> | grep enableExecuteCommand

Session Manager plugin をインストール

brew install --cask session-manager-plugin

コンテナーに exec する

aws ecs execute-command --cluster <value> --task <value> --container <value> --interactive --command "/bin/sh"

さいごに

作業が終わったら、ECS Exec は無効にしておきましょう

aws ecs update-service --service <value> --cluster <value> --disable-execute-command

Digdag から Amazon SNS 経由でエラーを通知する方法を考えた

Digdag から Amazon SNS 経由で Slack にエラーを通知するやつを今までは AWS Lambda を使ってやっていたが、Lambda を使うとコード管理どうするとか色々考えると面倒なので、今年使えるようになった AWS Chatbot が使えたら、いいなーって思ったので調査した

ただし、こちらにあるようにSNS -> Chatbotという経路では、使えません https://docs.aws.amazon.com/chatbot/latest/adminguide/related-services.html

でも簡単に設定できるので、一応試してみた
結果、できなかったが、やったことをメモ

Amazon SNS

トピックの作成

トピックの作成をクリックし、以下を設定

詳細

設定項目 設定値
タイプ スタンダード
名前 任意の名前

AWS Chatbot

Slack のワークスペースの設定

クライアントの設定でSlackを選択

f:id:kurosame-th:20201222101648p:plain

この後、Slack のワークスペースに Chatbot からのアクセスを許可を求める画面が出るので、許可する

チャネルの設定

新しいチャネルを設定をクリックし、以下を設定

設定の詳細

設定項目 設定値
設定名 任意の名前
ログ記録 Amazon CloudWatch Logs にログを発行する(エラーのみ)

Slack チャネル

設定項目 設定値
チャネル ID Slack のチャネル ID

Slack のチャネル ID の Slack の対象のチャネルを右クリックして、Copy linkで確認できる

アクセス許可

CloudWatch に関わるポリシーを付与した、IAM ロールが作成される

設定項目 設定値
IAM ロール 任意のロール名(新規にロールが作られる)
ポリシーテンプレート 通知のアクセス許可

以下のポリシーでアタッチされたロールが作成された

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": ["cloudwatch:Describe*", "cloudwatch:Get*", "cloudwatch:List*"],
      "Effect": "Allow",
      "Resource": "*"
    }
  ]
}

通知 - オプション

設定項目 設定値
SNS トピック 上記で作成した SNS トピック

AWS CLI

必要なポリシー

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "VisualEditor0",
      "Effect": "Allow",
      "Action": "sns:Publish",
      "Resource": "[作成した SNS トピックのARN]"
    }
  ]
}

実行

aws sns publish \
--topic-arn ${SNS トピックのARN} \
--message "SNS test message" \
--subject "SNS test subject" \
--region ap-northeast-1

以下のエラーが CloudWatch に出力された

Event received is not supported (see https://docs.aws.amazon.com/chatbot/latest/adminguide/related-services.html )

SNS -> Chatbotという連携はできないっぽい
まあ当然の結果ではありますが、、

たとえば、CloudWatch Alarms -> SNS -> Chatbotという経由であれば、できるのだと思う
ただ、今回のケースでは Digdag でエラーが起きる度に通知を飛ばしたいので、CloudWatch Alarm で閾値や期間を設定するのは、難しいのかなーと

Digdag を使っているので、現状だと解決方法は以下なのかな

Nuxt.js で Vuex を使わない場合に考えること

Vuex を使うのに慣れすぎて、Vuex を使わないパターンを考えた時に色々考えたのでメモ

方針

  • 比較的規模が小さいシステムでは、Vuex の利用をまずは避けたい
  • Nuxt.js を使用
  • Composition API を使用

コンポーネント間のデータ受け渡しパターン

親から子

  • 普通に props 経由

子から親

  • $emitで親の関数を呼ぶ
  • .sync修飾子と$emit.updateで親の状態を更新する

子から子

  • 禁止
  • 必ず親を経由する

親から親

たとえば、pages コンポーネントから pages コンポーネントにデータを渡したい時など

代替手段

  • URL に含めて vue-router から取る
    • 渡したいデータが増えた時の拡張性が低い
  • ブラウザのセッションストレージとかに入れとく
    • なんか微妙(TypeScript 使っているし)
  • Provide/Inject パターンを使う
    • Vue の 2.2.0 からあるらしい
    • Vuex の代わりになりそう
    • これは小規模なアプリケーションには、よい気がする

Provide/Inject パターンの実装

Nuxt.js と Composition API での実装

Key/Value を定義する
Value にグローバルで管理したい状態とロジックを定義する

// composables/dashboard.ts
import { Dashboard } from '@nuxt/types'
import { ref, Ref, InjectionKey } from '@nuxtjs/composition-api'

// Storeの型定義
export type DashboardStore = {
  dashboard: Ref<Dashboard>
  setDashboard: (d: Dashboard) => void
}

// Key
export const DashboardKey: InjectionKey<DashboardStore> = Symbol(
  'DashboardStore'
)

// Value
export const useDashboard = (): DashboardStore => {
  // 状態
  const dashboard = ref<Dashboard>({ dashboard_id: 0, dashboard_name: '' })
  // ロジック
  const setDashboard = (d: Dashboard) => (dashboard.value = d)

  return { dashboard, setDashboard }
}

次に、上記の Key/Value で provide する
今回は pages コンポーネント間でデータ受け渡しを行いたいので、layout コンポーネントで実装した

// layouts/default.vue
import { defineComponent, provide } from '@nuxtjs/composition-api'
import { DashboardKey, useDashboard } from '@/composables/dashboard'

export default defineComponent({
  setup() {
    provide(DashboardKey, useDashboard())
  }
})

状態を変更する場合

// pages/set.vue
import { defineComponent, inject } from '@nuxtjs/composition-api'
import { DashboardKey, DashboardStore } from '@/composables/dashboard'

export default defineComponent({
  setup() {
    const { setDashboard } = inject<DashboardStore>(DashboardKey) || {
      setDashboard: () => console.error('Not found setDashboard')
    }
    setDashboard({...})
  }
})

状態を取得する場合

// pages/get.vue
import { defineComponent, inject } from '@nuxtjs/composition-api'
import { DashboardKey, DashboardStore } from '@/composables/dashboard'

export default defineComponent({
  setup() {
    const { dashboard } = inject<DashboardStore>(DashboardKey) || {}
  }
})

データの種類と取得場所

静的なデータ

サーバーなどから 1 回取得したら、変更がないデータなど

「Nuxt の plugins で取得し、Nuxt のコンテキストに inject しておく」

  • plugins 配下のスクリプトはルートの Vue コンポーネントインスタンス化される前に実行される
  • SSR していれば、クライアントとサーバーでそれぞれ実行される
  • nuxt.config.jsの plugins オプションで対象のモジュールを定義する際、modeというオプションで、そのプラグインの実行をクライアントのみ、もしくは、サーバーのみと指定可能

これはいい感じな気がする

動的なデータ

たとえば、アプリケーション内で変更可能なデータなど

「親コンポーネントで状態を管理および、変更処理(サーバーからデータ取得など)を行う」

コンポーネントが肥大化する場合

コンポーネントが pages の場合