3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ReactAdvent Calendar 2024

Day 9

【React小ネタ】ユーザーの入力をawaitするウィザード風Hook

Last updated at Posted at 2024-12-08

「ステップごとに入力していき、全部入力したら確定する」というウィザード風のUIにおいて、入力終了時にpromiseresolveして値が返ってくる、というようなHookを実装できないかなと思って、やってみたものです。

このようなことが可能なのかを知りたいというモチベーションで最低限の実装をやってみたものであり、実用上の問題がないかは未検証です。

実装

まずはHookのほう。

useWizard.tsx
import {
  useState,
  useCallback,
  useEffect,
  HTMLInputTypeAttribute,
} from 'react';

const useWizard = (
  prompts: { label: string; inputType: HTMLInputTypeAttribute }[]
) => {
  const [isActive, setIsActive] = useState(false);
  const [step, setStep] = useState(0);
  const [values, setValues] = useState<string[]>(
    new Array(prompts.length).fill('')
  );
  const [resolver, setResolver] = useState<null | ((reuslt: string[]) => void)>(
    null
  );

  // ステップごとに入力させ、最終的に値を返す関数
  const getValues = useCallback(async () => {
    setIsActive(true);
    const promise = new Promise<string[]>((resolve) => {
      setResolver(() => (result: string[]) => {
        resolve(result);
      });
    });
    return promise;
  }, []);

  useEffect(() => {
    if (resolver && step === prompts.length) {
      // stepが進んでいき、最終ステップで「進む」を押したらresolveする。
      resolver(values);
      
      // 初期化
      setIsActive(false);
      setResolver(null);
      setStep(0);
      setValues(new Array(prompts.length).fill(''));
    }
  }, [resolver, step]);

  const Controller = useCallback(() => {
    if (!isActive || step === prompts.length) return null;
    // 入力フォームはpromiseの待機中にもstepに応じて変化
    return (
      <form>
        <label>{prompts[step].label}</label>
        <input
          type={prompts[step].inputType}
          defaultValue={values[step]}
          onChange={(e) => {
            setValues((prev) =>
              prev.map((v, i) => (i === step ? e.target.value : v))
            );
          }}
        />
        <button
          onClick={(e) => {
            e.preventDefault();
            setStep(step + 1);
          }}
        >
          進む
        </button>
        <button
          onClick={(e) => {
            e.preventDefault();
            setStep(step - 1);
          }}
        >
          戻る
        </button>
      </form>
    );
  }, [isActive, step]);

  return { getValues, Controller, step };
};

export default useWizard;

……getValuespromiseuseEffectで解決してるのが気持ち悪いですね。

利用側はこんな感じで使います。

App.tsx
import { useState } from 'react';
import useWizard from './hooks/useWizard';

function App() {
  // useWizard
  const { Controller, getValues } = useWizard([
    { label: '名前', inputType: 'text' },
    { label: 'メールアドレス', inputType: 'text' },
    { label: 'パスワード', inputType: 'password' },
  ]);
  
  const [values, setValues] = useState<string[]>();
  
  const handleClick = async () => {
    // 入力開始ボタンをクリックするとpromiseが開始されて、Controllerが表示される
    // 最後まで入力するとpromsieがresolveされてresultに値が入る
    const result = await getValues();
    setValues(result);
  };
  return (
    <>
      <button onClick={handleClick}>入力する</button>
      <Controller />
      {values && (
        <ul>
          {values.map((v, i) => (
            <li key={i}>{v}</li>
          ))}
        </ul>
      )}
    </>
  );
}

export default App;

こっちは割とシンプルです。

デモ

Stackblitzでの動作デモです。
入力するをクリックするとフォームが出てきて、進むを押して入力を進め、最後に入力内容を表示します。

終わりに

今度個人開発のWebアプリにカメラを使った入力支援機能を付けようと思っていて、その前段階として、「ユーザー入力をawaitしたい」が実現可能かを試してみました。
上記のデモみたいに地べたに配置してしまうとわけわかんなくなるので、モーダルやドロワーと組み合わせて使うことを想定してます。

promiseresolveをよそに持っていくという実装はかなりスパゲティ感があるのですが、ユーザー入力を関数化すると考えれば、利用側はすごくシンプルに保てるんじゃないかと思ってます。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?