1
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?

React で1文字ずつ出すUIをhooksで実装する

Posted at

1文字ずつ出すUI

Reactで実装していると、こういう1文字ずつ表示されるUIが欲しくなる時ありますよね?

output.gif

ただ、Yahoo(実質Google)で調べた限りだと、よく知らないJavaScriptの標準機能やよく分からないライブラリを使った結果しか自分は観測できませんでした(自分の検索能力が足りないだけかもしれません)。

スクリーンショット 2024-12-08 10.22.32.png

ChatGPT に聞いた感じでも、
https://chatgpt.com/share/6754f58d-ce98-8006-bcac-14a039779bf8
みた感じ、undefined が表示されるなど、少し挙動が変に見えました。

output_3s.gif

なので、自分で実装してみました。

いざ実装

今回は仕様上、3つの文字列を、ループで1文字ずつ表示させるようにしました。成果物はこちらをご覧になってください。

まずは、表示させたい文字列とオリジナルの全文の文字列をuseStateで、文字列の位置を useRef で定義します。

※最初は、useRef で定義せず実装せず上手くいきませんでした。ここら辺の知見がある方がいたら教えてくださると嬉しいです。

    const originalFirstContent = "記事:HTMXの正体が分からないので、オレオレHTMXを作ってみた"
    const [firstContent, setFirstContent] = useState("")
    const originalSecondContent = "記事:React に プルリクを送ったけど、マージされなかった話"
    const [secondContent, setSecondContent] = useState("")
    const originalThirdContent = "記事:4つ目のPRでようやく Next.js にコントリビュートできた話"
    const [thirdContent, setThirdContent] = useState("")
    const currentPos = useRef(0)

次に、useEffect で、1文字ずつuseStataを更新するロジックを書きます。
流れは、以下の感じです。

  1. setIntervalを作る
  2. 1で作ったsetIntervalで最初に文字列の初期化を行う
  3. 1文字ずつ更新する処理をpromise & sleep で実装する
  4. 3で作ったpromiseをPromise.allで実行する

では、やってみましょう!

1:setIntervalを作る

ここは簡単です。setInterval は setTimeout のループ版みたいなやつです。

    useEffect(() => {
        async function doType() {
            // ここに実装を書く
        }
        doType()
        const id = setInterval(async() => {
            doType()
        }, 5000)
        return () => {
            clearInterval(id)
        }
    }, [])

この doType は setInterval だと初期発火まで5秒待たないといかないので、useEffectのなかで1回呼び出しています。

2:1で作ったsetIntervalで最初に文字列の初期化を行う

1文字ずつ表示し終わった時に、文字をリセットする必要があるので、リセットの処理を行います。

    useEffect(() => {
        async function doType() {
+           setFirstContent("")
+           setSecondContent("")
+           setThirdContent("")
+           currentPos.current = 0
        }
        doType()
        const id = setInterval(async() => {
            doType()
        }, 5000)
        return () => {
            clearInterval(id)
        }
    }, [])

3:1文字ずつ更新する処理をpromise & sleep で実装する

sleep処理をいくつか加えている部分もありますが、いかが大体のコードになります。(無駄があったらすみません!)
基本的には、promiseのsleepをタイマーにして、その後のthenで更新をしている感じです。

        async function doType() {
            setFirstContent("")
            setSecondContent("")
            setThirdContent("")
            currentPos.current = 0
+           await new Promise((resolve) => setTimeout(resolve, 300))
+           const promises: (Promise<void>)[] = []
+           for(let i = 0; i < originalThirdContent.length; i++) {
+               const promise = new Promise<void>((resolve) => {
+                   setTimeout(resolve, i * 100)
+               }).then(() => {
+                   currentPos.current = currentPos.current + 1
+                   setFirstContent(originalFirstContent.slice(0, currentPos.current))
+                   setSecondContent(originalSecondContent.slice(0, currentPos.current))
+                   setThirdContent(originalThirdContent.slice(0, currentPos.current))
+               })
+               promises.push(promise)
+           }
        }

4:3で作ったpromiseをPromise.allで実行する

最後にpromiseを実行します。

        async function doType() {
            // ... 省略
                promises.push(promise)
            }
+           Promise.allSettled(promises)
+           await new Promise((resolve) => setTimeout(resolve, 1000))
        }

これでロジックは実装できました!

ロジックの全コード
    const originalFirstContent = "記事:HTMXの正体が分からないので、オレオレHTMXを作ってみた"
    const [firstContent, setFirstContent] = useState("")
    const originalSecondContent = "記事:React に プルリクを送ったけど、マージされなかった話"
    const [secondContent, setSecondContent] = useState("")
    const originalThirdContent = "記事:4つ目のPRでようやく Next.js にコントリビュートできた話"
    const [thirdContent, setThirdContent] = useState("")
    const currentPos = useRef(0)
    useEffect(() => {
        async function doType() {
            setFirstContent("")
            setSecondContent("")
            setThirdContent("")
            currentPos.current = 0
            await new Promise((resolve) => setTimeout(resolve, 300))
            const promises: (Promise<void>)[] = []
            for(let i = 0; i < originalThirdContent.length; i++) {
                const promise = new Promise<void>((resolve) => {
                    setTimeout(resolve, i * 100)
                }).then(() => {
                    currentPos.current = currentPos.current + 1
                    setFirstContent(originalFirstContent.slice(0, currentPos.current))
                    setSecondContent(originalSecondContent.slice(0, currentPos.current))
                    setThirdContent(originalThirdContent.slice(0, currentPos.current))
                })
                promises.push(promise)
            }
            Promise.allSettled(promises)
            await new Promise((resolve) => setTimeout(resolve, 1000))
        }
        doType()
        const id = setInterval(async() => {
            doType()
        }, 5000)
        return () => {
            clearInterval(id)
        }
    }, [])

表示部分は、以下のようにすれば表示されるはずです。

{ firstContent && (
    <span
        style={{color: "red"}}
    >
        <br/>{firstContent}
    </span>
)}

react の hooks だけで実装したい人の役に立てば幸いです。

最後に

個人開発で検索エンジンを作っています!
よかったらみて見てください!

1
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
1
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?