概要
一文字ずつ文字が表示されるチャットUI(Typewriter風のUI)をReactで作ってみました。
実際の動き
実際のコード
import React, { useEffect, useState } from 'react';
const TypewriterMessage = ({ text, onDone }: { text: string; onDone: () => void }) => {
const [displayed, setDisplayed] = useState('');
useEffect(() => {
// テキストが空の場合は、onDoneを呼び出して終了
if (text.length === 0) {
onDone();
return;
}
let i = 0;
// setInterval関数を使って100ミリ秒ごとに1文字ずつ表示
const timer = setInterval(() => {
// textの長さ以上になった場合は終了する
if (i >= text.length) {
clearInterval(timer);
onDone();
return;
}
// 更新用関数setDisplayedで1文字ずつ追加していく
const nextChar = text[i];
setDisplayed((prev) => {
const next = prev + nextChar;
return next;
});
i++;
}, 100); // 文字が出る間隔
return () => clearInterval(timer);
}, [text]);
return <p>{displayed}</p>;
};
export default function ChatUI() {
const [messages, setMessages] = useState<string[]>([]);
const [input, setInput] = useState('');
const [isTyping, setIsTyping] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// trim():文字列の両端から空白を除いた文字列を生成
// 入力が空の場合もしくはタイプライター風アニメーション表示中は、何もしない
if (!input.trim() || isTyping) return;
setIsTyping(true);
setMessages((prev) => [...prev, input]);
setInput('');
};
return (
<div>
<div>
{messages.map((msg, idx) => (
<TypewriterMessage key={idx} text={msg} onDone={() => setIsTyping(false)} />
))}
</div>
<form onSubmit={handleSubmit}>
<input value={input} onChange={(e) => setInput(e.target.value)} disabled={isTyping} />
<button type="submit" disabled={isTyping}>送信</button>
</form>
</div>
);
}
はまったこと
set関数に1文字ずつ追加していく処理の完成したコードは以下のようになっています。
const timer = setInterval(() => {
(中略)
// 更新用関数setDisplayedで1文字ずつ追加していく
const nextChar = text[i];
setDisplayed((prev) => {
const next = prev + nextChar;
return next;
});
i++;
}, 100);
初めは以下のように実装していました。
const timer = setInterval(() => {
(中略)
// 更新用関数setDisplayedで1文字ずつ追加していく
setDisplayed((prev) => prev + chars[i]);
i++;
}, 100);
しかしこの場合は、たとえば「あいうえお」と入力すると「あうえおundefined」のように、途中の文字が抜け代わりに最後、undefinedが追加されてしまうような状態になってしまっていました。
なぜこうなるのかについて、setDisplayed関数がすぐに実行されないことが原因のようでした。
実行されるまでの間に i++ が実行され、iの値が変わってしまう。そのため、2文字目が消えてしまい、意図しないundefinedが表示される不具合が発生する。という流れになってしまっていました。
完成版のコードの場合は、const nextChar = text[i]; が即時に実行されるので、
nextCharの値が確定します。そのため、setDisplayed内では値はズレることなくdisplayedが更新されるという流れになります。
状態を更新する関数の中で外部の変数を参照する際には注意が必要だということを学びました。
