React×TypeScriptで性格診断テスト作ってみた⚡️

## はじめに
普段はデザイン業務(お手伝いで時々コーディング業務)を行なっていますが、診断系のアプリケーションを作って欲しいという依頼があったため、ReactとTypeScriptで簡易的な性格診断アプリを作成してみました。

https://github.com/shin-coder/personality-analysis-app

今回は、この簡易アプリの作成手順の備忘録も兼ねて記事にまとめていくことにします。

## Vite環境下でReact×TypeScriptの導入
以下の手順で性格診断サイトのファイルを作成します。
![article-photo-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3960520/fe2314e8-17b9-1483-dd51-0513b18552ca.png)
そして、お馴染みのページへ…
![article-photo-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3960520/e4587f68-8b2b-dfc9-6365-0f75d27cc5ff.jpeg)

## shadcn/uiの導入
今回、コンポーネント周りはshadcn/uiと呼ばれるコンポーネントライブラリを使用してみようと思います。
shadcn/uiはtailwindCSSでスタイリングできるのでReactとの相性も良いです。
https://ui.shadcn.com/

導入方法は記事が長くなってしまうので、少し割愛しながら導入していきます。
詳しくはこちらのドキュメントを参照してください。
https://ui.shadcn.com/docs/installation/vite

1\. tailwindCSSをインストール
`npm install -D tailwindcss postcss autoprefixer`
`npx tailwindcss init -p` で、
tailwindCSSをインストールと初期化を行います。

2\. configファイルの編集
ドキュメントに従い、`tsconfig.json` `tsconfig.app.json` `vite.config.ts`のファイルを編集していきます。

3\. shadcn/uiをインストール
`npx shadcn@latest init`で、
shadcn/uiをインストールと初期化を行います。

以上が完了したら、適宜必要なコンポートをインストールすることで利用できます。
試しにボタンを追加してみます。
![article-photo-3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3960520/9ed9f085-f91d-40dd-3611-3c106b534a1a.jpeg)
無事追加されました。

## 診断テスト作成
環境構築も完了したので、早速診断テストを作成していきます。

**アプリの仕様について**
本アプリは、各質問に対して点数を配分し、最高得点だった結果を出力するという比較的更新性の高いものを設計しました。

### データの準備
はじめに今回使用するデータ(質問/選択欄、診断結果）をsrc内にdataフォルダを作成し、各tsファイルに格納していきます。
※参考例のため説明は割愛します。
![article-photo-4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3960520/17bc337f-44a6-7093-74f2-73a99ac6a80e.jpeg)
![article-photo-5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3960520/f52bb932-3f80-4828-2f4d-2439c47b9219.jpeg)

### App.tsxへの記述
今回はuseStateで、状態管理していくので、それらを記述していきます。

**useSatetのおさらい**
Reactのコンポーネントは、状態が変化しないとコンポーネントが再レンダリングされません。
簡単な例で示すと、以下のようなカウントアップボタンを作成した時、ログではカウントアップされていますが、画面は更新されていないのが分かります。
```typescript:CountUp
import { Button } from "./components/ui/button";

function App() {

  let foo = 1;
  const handleClick = () => {
    foo = foo + 1;
    console.log(foo);
  };
  return (
    <>
      <div className="grid place-content-center gap-3 h-[100vh]">
        <h1 className="text-center text-2xl">{foo}</h1>
        <Button onClick={handleClick}>Click</Button>
      </div>
    </>
  );
}

export default App;
```
![article-photo-6.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3960520/669d20e4-0f4b-51fd-638e-09593bbfa360.jpeg)
これを変化させ、再レンダリングができるようになるためにuseSateでStateの管理が必要になります。
実際に記述してみると
```typescript:CountUp
const [foo, setFoo] = useState(1);

  const handleClick = () => {
    const newFoo = foo + 1;
    setFoo(newFoo);
  };
```
![article-photo-7.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3960520/9626e4ea-555b-1b26-549f-eddc5e8b04e1.jpeg)
これで無事、再レンダリングされました。

さて、本題に戻ります...

### Sate管理の定義
State管理させたいもの(質問内容、点数、結果表示の真偽値、診断結果)をuseSateで記述しておきます。
```typescript:src/App.tsx
const [currentQuestion, setCurrentQuestion] = useState(0);
// 診断結果が５つなので、配列を５つ用意し、その値を0にする
const [score, setScore] = useState(Array(5).fill(0));
const [showResuilt, setShowResult] = useState(false);
const [result, setResult] = useState(null);
```
### 診断アプリで機能させる関数を定義
続いて、状態管理に関する関数を定義していきます。
定義する関数では、
1\. 質問の選択欄(ボタン)を クリックすると、点数が割り振られ、
2\. 次の問題があれば先に進み、
3\. 最終問題であれば結果の真偽値を反転させ、
4\. 最高得点だったタイプを結果として出力する
というものを記述していきます。

```typescript:src/App.tsx
const handleAnswer = (points: number[]) => {
    // ①現在のスコア値をmap関数で展開し、それぞれにpointsの値を足す。それを新しいスコア値としてsetScoreに渡す。
    const newScore = score.map((s, index) => s + points[index]);
    setScore(newScore);
    // ②次の問題があれば先に進み、③最終問題であれば結果の真偽値を反転させる
    if (currentQuestion < questions.length - 1) {
      setCurrentQuestion(currentQuestion + 1);
    } else {
      setShowResult(true);
      // ④最高得点だったタイプを結果として出力する
      const maxScore = Math.max(...newScore);
      const resultPersonal = personal[newScore.indexOf(maxScore)];
      setResult(resultPersonal);
    }
  };
```
また、結果を初期値に戻す関数も用意しておきます。
```typescript:src/App.tsx
const reset = () => {
    setCurrentQuestion(0);
    setScore(Array(5).fill(0));
    setShowResult(false);
    setResult(null);
};
```

### フロント側の実装
ここからはtailwinCSSやshadcn/uiを使って画面の作り込みを行います。細いクラス名の説明などは割愛しますので、適宜こちらのドキュメントを参照してください
https://tailwindcss.com/docs/installation

```typescript:src/App.tsx
   return (
    <>
      <div className="w-full h-[100vh] grid place-items-center px-[5.5%]">
        <Card className="max-w-[600px] w-full mx-auto grid place-items-center">
          <CardContent className="px-[1rem] py-[2rem]">
            {/* 三項演算子で、showResultがtrueなら結果を表示、falseなら質問を表示 */}
            {!showResult ? (
              <div>
                <h2 className="text-center">
                  質問{currentQuestion + 1} / {questions.length}
                </h2>
                <p className="mt-[1.5rem] text-center font-bold">
                  {questions[currentQuestion].text}
                </p>
                <div>
                  {questions[currentQuestion].options.map((option, index) => (
                    <Button
                      key={index}
                      className="mt-[1rem] w-full py-[1.5rem] text-[0.875rem]"
                      onClick={() => handleAnswer(option.points)}
                    >
                      {option.text}
                    </Button>
                  ))}
                </div>
              </div>
            ) : (
              <div className="text-center">
                <h2 className="text-[1.5rem] font-bold">診断結果</h2>
                <div>
                  <h3 className="mt-[1rem] font-bold">{result?.type}</h3>
                  <p className="mt-[0.875rem]">{result?.features}</p>
                  <p>{result?.enviroment}</p>
                  <p>{result?.advice}</p>
                </div>
                <Button className="mt-[2rem]" onClick={reset}>
                  もう一度診断する
                </Button>
              </div>
            )}
          </CardContent>
        </Card>
      </div>
    </>
  );
  ```
これで完成...
<br>

といきたいところですが、どうやらエラーを吐いているので、解決していきます。
![article-photo-8.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3960520/c4b7fd22-6384-001f-5a2f-9606aba569e9.jpeg)
ここのエラー文を見ると、
>Argument of type 'Personal' is not assignable to parameter of type 'SetStateAction<null>'.Type 'Personal' provides no match for the signature '(prevState: null): null'.

と書いてありました。要するに、
_**const [result, setResult] = useState(null);がnullと定義されているのに、他の値を代入しようとしているから、型に一致しません**_
ということなので、ユニオン型でresult が null または Personal 型のどちらかの値を持てるように定義していきます。
```typescript:src/App.tsx
 const [result, setResult] = useState<Personal | null>(null);
 ```
で解決しました。

## おわりに
Stateの管理やそれを関数にどう定義していくかかなり苦戦しましたが、何とか形に出来ました。WEB制作とはまた違ったアプローチ手法でしたが、JSの深い知識が身についたと感じたので、これからも何かしらアプリケーションが作れたらと思ってます！