1文字ずつ出すUI
Reactで実装していると、こういう1文字ずつ表示されるUIが欲しくなる時ありますよね?
ただ、Yahoo(実質Google)で調べた限りだと、よく知らないJavaScriptの標準機能やよく分からないライブラリを使った結果しか自分は観測できませんでした(自分の検索能力が足りないだけかもしれません)。
ChatGPT に聞いた感じでも、
https://chatgpt.com/share/6754f58d-ce98-8006-bcac-14a039779bf8
みた感じ、undefined が表示されるなど、少し挙動が変に見えました。
なので、自分で実装してみました。
いざ実装
今回は仕様上、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を更新するロジックを書きます。
流れは、以下の感じです。
- setIntervalを作る
- 1で作ったsetIntervalで最初に文字列の初期化を行う
- 1文字ずつ更新する処理をpromise & sleep で実装する
- 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 だけで実装したい人の役に立てば幸いです。
最後に
個人開発で検索エンジンを作っています!
よかったらみて見てください!