Vite App+Vue3でクソアプリを作った

これはクソアプリ2アドベントカレンダー11日目の記事です。
今回、9月に正式リリースされたVue3を使ってとてつもなくシンプルなクソアプリを作ったので、その制作について話をします。

## 作ったアプリ

- URL：https://wyn.whyk.dev/
- リポジトリ：https://github.com/windchime-yk/whats-your-name

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197494/ffc44d6f-70b3-97a5-c90c-958c30e358c6.png)

## このアプリのクソポイント

- UIがクソ
  - ろくすっぽスタイルがあたってません（そもそも要素が少ないというのもある）
- 機能がクソ
  - 機能は、入力された名前にランダム文字列を追加してAPIに投げ、画像を取得して表示する**だけ**です

## このアプリの制作趣意

今回は、Vite Appを使い、Vue3とTypeScriptで、以下を学ぶことを目標としていました。

- Composition API
- Provide/Inject

## 制作過程

### 環境構築

まず、Vite AppでVue3環境を構築します。
TypeScriptを使いたいので、`--template`フラグを利用してTypeScriptのテンプレートを利用します。

``` bash
$ npm init vite-app <project-name> -- --template vue-ts
# or
$ yarn create vite-app <project-name> --template vue-ts
```

この時点でのディレクトリ構造はこちら

``` bash
.
├── index.html
├── package.json
├── public
│   └── favicon.ico
├── src
│   ├── App.vue
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   └── HelloWorld.vue
│   ├── index.css
│   ├── main.ts
│   └── vue-app-env.d.ts
├── tsconfig.json
└── yarn.lock
```

ここからこねこねして、最終的に以下のディレクトリ構造になりました

``` bash
.
├── README.md
├── index.html
├── package.json
├── src
│   ├── App.vue
│   ├── assets
│   │   └── style
│   ├── components
│   │   ├── AnswerBtn.vue
│   │   ├── InputName.vue
│   │   ├── ResetBtn.vue
│   │   ├── SwichingView.vue
│   │   └── UserImage.vue
│   ├── main.ts
│   ├── store
│   │   └── useState
│   │       ├── model.ts
│   │       └── provider.ts
│   └── vue-app-env.d.ts
├── tsconfig.json
└── yarn.lock
```

### 親コンポーネントにProvideを設置

前述の通り、今回のアプリの状態管理にはProvide/Injectを採用してます。これは親コンポーネントの状態を子コンポーネントに状態を受け渡すことができる仕組みで、Propsと違って子コンポーネントであればどこまでも受け渡すことができるので、Propsのようなバケツリレーをすることがありません。
正直、今回の構成だとPropsで問題ないんですが、これを試すのが目的なので、使っています。

まず親コンポーネントで、受け渡す値を`useState`という関数にまとめて別ファイルで変数に格納します。

``` typescript
export const useState = () => {
  const state = reactive({
    name: 'whyk',
    newname: '',
    answered: false,
    clicked: false,
  })

  const updateName = (newname: string) => {
    const randString = Math.random().toString(32).substring(2)
    state.name = `${newname}_${randString}`
  }
  const updateAnswered = () => {
    state.answered = !state.answered
  }
  const updateClicked = () => {
    state.clicked = true
  }
  const resetState = () => {
    state.name = 'whyk'
    state.newname = ''
    state.answered = false
    state.clicked = false
  }

  return {
    state,
    updateName,
    updateAnswered,
    updateClicked,
    resetState,
  }
}
```

また、この関数を識別するキーとして、シンボル化した値を変数に格納します。
型については、`InjectionKey`はProvide/Injectのキーとして利用される型定義、型変数には`StateStore`という、また別の場所で作成された上記関数の返り値を型として利用できるようにしたものになります。

``` typescript
export const StateKey: InjectionKey<StateStore> = Symbol('StateStore')
```

これらを親コンポーネントである`src/App.vue`で読み込みます。

``` typescript
import { defineComponent, provide } from 'vue'
import { useState, StateKey } from './store/useState/provider'

export default defineComponent({
  setup() {
    provide(StateKey, useState())
    return {}
  },
})
```

これで、子コンポーネントへの状態の受け渡しの準備が整いました。

### 子コンポーネントにInjectを設置

次に、子コンポーネントを作成し、親コンポーネントから状態を受け取ります。

``` typescript
import { defineComponent, inject } from 'vue'
import type { StateStore } from '../store/useState/model'
import { StateKey } from '../store/useState/provider'

export default defineComponent({
  setup() {
    const { state } = inject(StateKey) as StateStore

    return {
      state,
    }
  },
})
```

Vueから提供されている`inject`にProvide/Injectのキーを渡し、型アサーションで型を`useState`の返り値で上書きします。
これで親コンポーネントから状態が受け取れるようになりました。

### そのあと

Provide/Injectを使う関係で、すべての要素を子コンポーネントにする必要があります。
そのため、最終的な親コンポーネントは以下のようになっています。

``` vue
<template>
  <user-image />
  <h1>Whats Your Name?</h1>
  <switching-view>
    <template #question>
      <input-name />
      <answer-btn />
    </template>
    <template #answer>
      <p>Hahaha!! Very suitable!!!!</p>
      <reset-btn />
    </template>
  </switching-view>
</template>

<script lang="ts">
import { defineComponent, provide } from 'vue'
import SwitchingView from './components/SwichingView.vue'
import UserImage from './components/UserImage.vue'
import InputName from './components/InputName.vue'
import AnswerBtn from './components/AnswerBtn.vue'
import ResetBtn from './components/ResetBtn.vue'
import { useState, StateKey } from './store/useState/provider'

export default defineComponent({
  components: {
    SwitchingView,
    UserImage,
    InputName,
    AnswerBtn,
    ResetBtn,
  },
  setup() {
    provide(StateKey, useState())
    return {}
  },
})
</script>
```

## 最後に

Advent Calendar初参加でしたが、本来出すはずのクソアプリが完成せず、急きょ別のクソアプリで記事を書きました。
ここに出すはずだったクソアプリを作るプランも並行して考えているので、また違うフレームワークで作ってみたいところです。