0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Typewriter風チャットUI

Posted at

概要

一文字ずつ文字が表示されるチャットUI(Typewriter風のUI)をReactで作ってみました。

実際の動き

Typewriter風のUI.gif

実際のコード

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が更新されるという流れになります。

状態を更新する関数の中で外部の変数を参照する際には注意が必要だということを学びました。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?