序
「ステップごとに入力していき、全部入力したら確定する」というウィザード風のUIにおいて、入力終了時にpromise
がresolve
して値が返ってくる、というようなHook
を実装できないかなと思って、やってみたものです。
このようなことが可能なのかを知りたいというモチベーションで最低限の実装をやってみたものであり、実用上の問題がないかは未検証です。
実装
まずはHook
のほう。
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;
……getValues
のpromise
をuseEffect
で解決してるのが気持ち悪いですね。
利用側はこんな感じで使います。
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したい」が実現可能かを試してみました。
上記のデモみたいに地べたに配置してしまうとわけわかんなくなるので、モーダルやドロワーと組み合わせて使うことを想定してます。
promise
のresolve
をよそに持っていくという実装はかなりスパゲティ感があるのですが、ユーザー入力を関数化すると考えれば、利用側はすごくシンプルに保てるんじゃないかと思ってます。