【實作記錄】Typing Speed Test | React

Live Demo
Github

Typing Speed Test App Demo

簡介

Typing Speed Test App 有基本的倒數計算字數的功能。運用了 React Hook 的 useState, useEffect 以及 useRef. 最後把 hook 獨立出來,讓程式碼易讀性更高。完整的程式碼附在文末。

功能

  • 點擊按鈕開始計時,開始後無法再點擊按鈕
  • 倒數結束,textarea 無法再輸入
  • 字數與單個字數隨著用戶輸入不停更新

步驟

基本設置

設置基本樣式,包含輸入的地方 (textarea)、倒數器、每分鐘打了幾個字、每分鐘輸入了幾個字母、開始按鈕 (button).


增加 state

  • 為 textarea 增加 state
  • 用戶在輸入的時候,textarea 的 value 產生改變
App.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import React, { useState } from "react"
import './App.css';
import "./index.css"

//增加 state
function App() {
const [text, setText] = useState("")

function handleChange(e){
const {value} = e.target
setText(value)
}

return (
<div className="container">
<div className="head">
<header>
<p className="subtile">TYPING SPEED TEST</p>
<h1>How <span>fast</span> do you type?</h1>
</header>

<div className="result">
<div className="time">
<p className="num">60</p>
<p>seconds</p>
</div>

<div className="words">
<p className="num num-result border">0</p>
<p className="font-result">words/min</p>
</div>

<div className="chars">
<p className="num num-result border">0</p>
<p className="font-result">chars/min</p>
</div>
</div>
</div>

<textarea className="area"
value={text}
type="text"
name="typingArea"
onChange={handleChange}
placeholder={"Tales from the Galaxy’s Edge put players in the role of a droid-repair technician who crash landed on Batuu following a pirate attack.}
/>

<button type="submit">Start</button>
<p>{text}</p> //測試是否有成功增加
</div>
);
}

export default App;

算字數和單個字母

  • 以空格分開,把輸入的字存進 array 裡,最後返回 array 的長度
  • 字母同理
App.js
1
2
3
4
5
6
7
8
function wordsCount(keyInText){
const wordsArr = text.trim().split(" ")
if (text !== ""){
return wordsArr.length
}else{
return 0
}
}

倒數計時器

  • 使用 useEffect 搭配 setTimeout 處理
  • 倒數開始:按下 start button 後 & 當秒數大於 0 時
  • 倒數結束:isTimeRunning state 要回到 false
App.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const [seconds, setSeconds] = useState(10)
const [isTimeRunning, setisTimeRunning] = useState(false)

useEffect(() => {
if (isTimeRunning &&seconds > 0){
setTimeout(() => {
setSeconds(prevsec => prevsec - 1)
}, 1000)
}else if (seconds === 0){
setisTimeRunning(false)
}

},[seconds, isTimeRunning])

<button onClick={() => setisTimeRunning(() => true)}>Start</button>

Result 同步更新

  • 用戶一遍輸入,result 一遍更新
App.js
1
2
3
4
5
6
7
8
9
const [word, setWord] = useState(0)
const [char, setChar] = useState(0)

function countingWords(){
setWord(wordsCount(text))
setChar(charsCount(text))
}

useEffect(countingWords,[text])

遊戲開始/進行時

  • StartGame() : 遊戲開始時初始化所有東西
  • useRef : 可以在不重新 Render 的狀況下更新值
    • 遊戲開始時,自動 focus textarea, 用戶不用點擊就能開始打字
    • 因為在 textarea 中設定了計時器尚開始前,處在 disabled 的狀態
    • 所以如果只寫 textboxRef.current.focus(),其實是 focus 在 disabled 的狀態,點擊開始時不會自動 focus textarea
    • 可以加上一行 textboxRef.current.disabled = false 解決這個問題
App.js
1
2
3
4
5
6
7
8
9
const STARTING_TIME = 60

function startGame(){
setSeconds(STARTING_TIME)
setIsTimeRunning(true)
setText("")
textboxRef.current.disabled = false
textboxRef.current.focus()
}
  • button disabled : 遊戲進行時(計時器開始後),button 無法點擊
App.js
1
<button onClick={startGame} disabled={isTimeRunning}>Start</button>
  • textarea disabled : 遊戲結束時(計時器結束時), textarea 不能再輸入
App.js
1
2
3
4
5
6
7
8
<textarea className="area"
value={text}
type="text"
name="typingArea"
onChange={handleChange}
disabled={!isTimeRunning}
ref={textboxRef}
/>

custom hook

首先把原本在 App.js 建立好的各個 function 拆出放到另一個檔案(useWordGame.js),在這個檔案中要定義 custom hook. (參考下面完整的程式碼)把其他 component 需要用到的值或是 function 回傳。

useWordGame.js
1
return {seconds, word, char, text, handleChange, isTimeRunning, textboxRef, startGame}

App.js 中需要使用到的值以及 function 就可以使用剛剛定義好的 hooks, 就跟使用其他 hooks 一樣,接著就可以運用這些值 / function 了。

App.js
1
const { seconds, word, char, text, handleChange, isTimeRunning, textboxRef, startGame } = useWordGame()

總結

有好幾個需要的功能還沒放進去,先在這裡留個記錄,之後有機會再回來補。但經過這個小練習,也更熟悉 hook 的基本用法,希望之後可以挑戰更難得耶耶。

這篇筆記為個人學習記錄,若有錯誤或是可以改進的地方再麻煩各位大大指點(鞠躬


參考資料

Hooks API Reference : useRef
【Day 24】 useRef


完整程式碼

App.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import React from "react"
import useWordGame from "./useWordGame"
import "./index.css"

function App() {

const {
seconds,
word,
char,
text,
handleChange,
isTimeRunning,
textboxRef,
startGame
} = useWordGame()

const article =
"Tales from the Galaxy’s Edge put players in the role of a droid-repair technician who crash landed on Batuu following a pirate attack. Upon entering a cantina owned by Seezelslak (played by Saturday Night Live’s and Star Wars Resistance’s Bobby Moynihan), players were swept up into an adventure that found them exploring Batuu and interacting with Star Wars characters new and classic. Part II looks to expand on that, starting with Dok-Ondar."

return (
<div className="container">
<div className="head">
<header>
<p className="subtile">TYPING SPEED TEST</p>
<h1>How <span>fast</span> do you type?</h1>
</header>

<hr/>

<div className="result">
<div className="time">
<p className="num">{seconds}</p>
<p>seconds</p>
</div>

<div className="words">
<p className="num num-result border">{word}</p>
<p className="font-result">words/min</p>
</div>

<div className="chars">
<p className="num num-result border">{char}</p>
<p className="font-result">chars/min</p>
</div>
</div>
</div>

<p className="article">{article}</p>

<textarea className="area"
value={text}
type="text"
name="typingArea"
onChange={handleChange}
disabled={!isTimeRunning}
ref={textboxRef}
placeholder={"Start your test now!"}
/>

<button
onClick={startGame}
disabled={isTimeRunning}
>
Start
</button>

</div>

);
}

export default App;

useWordGame.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import { useState, useEffect, useRef } from "react"

function useWordGame(){
const STARTING_TIME = 60

const [text, setText] = useState("")
const [seconds, setSeconds] = useState(STARTING_TIME)
const [isTimeRunning, setIsTimeRunning] = useState(false)
const [word, setWord] = useState(0)
const [char, setChar] = useState(0)
const textboxRef = useRef(null)

//input 改變
function handleChange(e){
const {value} = e.target
setText(value)
}

//計算word
function wordsCount(keyInText){
const wordsArr = text.trim().split(" ")
if (text !== ""){
return wordsArr.length
}else{
return 0
}
}

//計算character
function charsCount(keyInText){
const charsArr = text.trim().split("")
if (text !== ""){
return charsArr.length
}else{
return 0
}
}

//遊戲開始
function startGame(){
setSeconds(STARTING_TIME)
setIsTimeRunning(true)
setText("")
textboxRef.current.disabled = false
textboxRef.current.focus()
}

function countingWords(){
setWord(wordsCount(text))
setChar(charsCount(text))
}

//倒數
useEffect(() => {
if (isTimeRunning &&seconds > 0){
setTimeout(() => {
setSeconds(prevsec => prevsec - 1)
}, 1000)
} else if (seconds === 0){
setIsTimeRunning(false)
}
},[seconds, isTimeRunning])

useEffect(countingWords,[text])

return {seconds, word, char, text, handleChange, isTimeRunning, textboxRef, startGame}
}

export default useWordGame

Comments