質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
React.js

Reactは、アプリケーションのインターフェースを構築するためのオープンソースJavaScriptライブラリです。

Q&A

解決済

1回答

884閲覧

【React】タイピングアプリ(Web)の単語の遷移で、「前の単語に戻る」ボタンのバグを解消したいです

panda33

総合スコア5

React.js

Reactは、アプリケーションのインターフェースを構築するためのオープンソースJavaScriptライブラリです。

0グッド

0クリップ

投稿2021/09/03 07:22

前提・実現したいこと

ReactでWebのタイピングアプリを製作したく、Codesandboxにて少しずつコードを書いています。
現在は、タイピングが完了した前の単語に戻るボタンを設置しようとしているのですが、
問題が発生したため、お力添えをいただけませんでしょうか。

下記がCodesandboxのリンクとなります。
https://codesandbox.io/s/typing-test-fgzfr?file=/src/App.tsx:0-3080
※プログラミング歴がまだ2か月のため、的外れな文章や汚いコード(全然typescriptの書き方じゃないなど)恐縮です。

発生している問題・エラーメッセージ

・「前の単語に戻る」ための戻るボタンを押した際に、先の単語に進んでしまうことがある。
・一度戻るボタンを使用すると、タイピング完了時に「次の単語に進む」メソッドが実行された際にも、前の単語に戻ってしまうことがある。

該当のソースコード

React typescript

import React, { useState, useEffect, useRef } from "react"; import "./App.scss"; export default function App() { //問題例 const wordList = [ "tora", "raion", "ookami", "baison", "kitsune", "usagi", "inu", "neko", "ningen", "hitsuji" ]; //ルビ const rubiList = [ "とら", "らいおん", "おおかみ", "ばいそん", "きつね", "うさぎ", "いぬ", "ねこ", "にんげん", "ひつじ" ]; //漢字 const anserList = [ "虎", "ライオン", "狼", "バイソン", "狐", "ウサギ", "犬", "猫", "人間", "羊" ]; //タイピングに関する部分 const [number, setNumber] = useState(1); const [anser, setAnser] = useState(anserList[0]); const [rubi, setRubi] = useState(rubiList[0]); const [text, setText] = useState(wordList[0]); const [position, setPosition] = useState(0); const [typo, setTypo] = useState(new Array(0)); const handleKey = (e: React.KeyboardEvent<HTMLDivElement>) => { // 文字の配列を取得 let textSpans = document.querySelector("#textbox").children; // 入力したキーと現在入力しようとしている文字が一致するとき if (e.key === text[position]) { // 現在の文字を入力済とする textSpans[position].className = "typed-letters"; textSpans[position].classList.remove("current-letter"); // まだ入力していない文字があるとき if (position <= text.length - 2) { // 次の位置へ移動 textSpans[position + 1].className = "current-letter"; setPosition(position + 1); // 全ての文字を入力し終わったとき } else { setNumber(number + 1); setText(wordList[number]); setAnser(anserList[number]); setRubi(rubiList[number]); setPosition(0); textSpans[0].className = "current-letter"; for (let i = 1; i < text.length; i++) { textSpans[i].className = " waiting-letters"; console.log(number); } } // 間違ったキーを入力したとき } else { if (typo.indexOf(position) === -1) { setTypo([...typo, position]); textSpans[position].classList.add("typo"); } } }; //入力エリアにフォーカスを当てる const searchInput = useRef(null); useEffect(() => { searchInput.current.focus(); }); //戻るボタン const backButton = () => { setNumber(number - 1); setText(wordList[number]); setAnser(anserList[number]); setRubi(rubiList[number]); setPosition(0); console.log(number); }; return ( <> <div ref={searchInput} className="App" onKeyPress={(e) => handleKey(e)} tabIndex={0} > <h1> {/* 問題(漢字) */} <ruby> {anser} <rt>{rubi}</rt> </ruby> </h1> {/* 入力部分 */} <div id="textbox"> <span className="current-letter">{text[0]}</span> {text .split("") .slice(1) .map((char) => ( <span className="waiting-letters">{char}</span> ))} </div> {/* 戻るボタン */} <button onClick={() => { backButton(); }} > 戻る </button> </div> </> ); }

試したこと

再レンダリングのために問題が発生しているのかと思い、memoやcallbackを使用してみましたが、解決できませんでした(やり方が間違っていた可能性はありますが...)。
なお、将来的にはRailsの中に埋め込む形で使用したいと考えているので、rooterは使用しないでこの中で完結する形で単語の遷移がしたいです。

もしお気づきのことがございましたら、ご教授いただけますと幸いです。
宜しくお願い致します。

補足情報(FW/ツールのバージョンなど)

下記がCodesandboxのリンクとなります。
https://codesandbox.io/s/typing-test-fgzfr?file=/src/App.tsx:0-3080

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

stateは即時更新されません。
再レンダリング時のuseStateのタイミングで更新されます。

js

1 setNumber(number - 1); 2 setText(wordList[number]); 3 setAnser(anserList[number]); 4 setRubi(rubiList[number]);

なので上のような書き方だと、numbernumber-1で更新することを要求はしているが、setText等では古い(更新前)のnumberをインデックスとして使用することになります。

更にnumber+1の更新も同じ問題が生じているため、numbertext等のインデックス間に齟齬が生じています。

そのため、戻るボタンを押してnumber-1をしても、textなどにはnumber+1で以前更新された値を使用するため先の単語に進むことになってしまっています。

解決策1

numberの更新する値を別変数として保持し、その値を使用してtext等も更新する。
今回は更新箇所が2箇所有るので関数の引数にしていますが、通常の変数でも問題ありません。

js

1const wordTransition = (nextNum: number) => { 2 setNumber(nextNum); 3 setText(wordList[nextNum]); 4 setAnser(anserList[nextNum]); 5 setRubi(rubiList[nextNum]); 6 setPosition(0); 7 console.log(nextNum); 8 }; 9

js

1 // 全ての文字を入力し終わったとき 2 } else { 3 wordTransition(number + 1); 4 textSpans[0].className = "current-letter"; 5 for (let i = 1; i < text.length; i++) { 6 textSpans[i].className = " waiting-letters"; 7 } 8 }

js

1 2//戻るボタン 3const backButton = () => { 4 wordTransition(number - 1); 5}; 6

解決策2

textanswernumberから一意に導入できる値なため、stateとして保持すべきものではありません。
state更新に合わせて画面の状態も更新させたい状態の保持に使用するものです。今回の例だとnumber,positionのみがその対象に見えます。

text,answer,rubianserList[number]などのようにnumberをインデックスとして毎回導入するか、通常の変数として保持します。

typoは定義している意味がわかりませんでした。無くても動きます。

DIFF

1 //タイピングに関する部分 2 const [number, setNumber] = useState(0); 3- const [anser, setAnser] = useState(anserList[0]); 4- const [rubi, setRubi] = useState(rubiList[0]); 5- const [text, setText] = useState(wordList[0]); 6 const [position, setPosition] = useState(0); 7- const [typo, setTypo] = useState(new Array(0)); 8 9 const wordTransition = (nextNum: number) => { 10 setNumber(nextNum); 11- setText(wordList[nextNum]); 12- setAnser(anserList[nextNum]); 13- setRubi(rubiList[nextNum]); 14 setPosition(0); 15 console.log(nextNum); 16 };

全てのコード

js

1import React, { useState, useEffect, useRef } from "react"; 2import "./App.scss"; 3 4export default function App() { 5 //問題例 6 const wordList = [ 7 "tora", 8 "raion", 9 "ookami", 10 "baison", 11 "kitsune", 12 "usagi", 13 "inu", 14 "neko", 15 "ningen", 16 "hitsuji" 17 ]; 18 19 //ルビ 20 const rubiList = [ 21 "とら", 22 "らいおん", 23 "おおかみ", 24 "ばいそん", 25 "きつね", 26 "うさぎ", 27 "いぬ", 28 "ねこ", 29 "にんげん", 30 "ひつじ" 31 ]; 32 33 //漢字 34 const answerList = [ 35 "虎", 36 "ライオン", 37 "狼", 38 "バイソン", 39 "狐", 40 "ウサギ", 41 "犬", 42 "猫", 43 "人間", 44 "羊" 45 ]; 46 47 //タイピングに関する部分 48 const [number, setNumber] = useState(0); 49 const [position, setPosition] = useState(0); 50 51 const text = wordList[number]; 52 const answer = answerList[number]; 53 const rubi = rubiList[number]; 54 55 // 単語の変更 56 const wordTransition = (nextNum: number) => { 57 setNumber(nextNum); 58 setPosition(0); 59 console.log(nextNum); 60 }; 61 62 const handleKey = (e: React.KeyboardEvent<HTMLDivElement>) => { 63 // 文字の配列を取得 64 let textSpans = document.querySelector("#textbox").children; 65 66 // 入力したキーと現在入力しようとしている文字が一致するとき 67 if (e.key === text[position]) { 68 // 現在の文字を入力済とする 69 textSpans[position].className = "typed-letters"; 70 textSpans[position].classList.remove("current-letter"); 71 72 // まだ入力していない文字があるとき 73 if (position <= text.length - 2) { 74 // 次の位置へ移動 75 textSpans[position + 1].className = "current-letter"; 76 setPosition(position + 1); 77 // 全ての文字を入力し終わったとき 78 } else { 79 wordTransition(number + 1); 80 textSpans[0].className = "current-letter"; 81 for (let i = 1; i < text.length; i++) { 82 textSpans[i].className = " waiting-letters"; 83 } 84 } 85 // 間違ったキーを入力したとき 86 } else { 87 textSpans[position].classList.add("typo"); 88 } 89 }; 90 91 //入力エリアにフォーカスを当てる 92 const searchInput = useRef(null); 93 useEffect(() => { 94 searchInput.current.focus(); 95 }); 96 97 //戻るボタン 98 const backButton = () => { 99 wordTransition(number - 1); 100 }; 101 102 return ( 103 <> 104 <div 105 ref={searchInput} 106 className="App" 107 onKeyPress={(e) => handleKey(e)} 108 tabIndex={0} 109 > 110 <h1> 111 {/* 問題(漢字) */} 112 <ruby> 113 {answer} 114 <rt>{rubi}</rt> 115 </ruby> 116 </h1> 117 {/* 入力部分 */} 118 <div id="textbox"> 119 <span className="current-letter">{text[0]}</span> 120 {text 121 .split("") 122 .slice(1) 123 .map((char, index) => ( 124 <span className="waiting-letters" key={index}> 125 {char} 126 </span> 127 ))} 128 </div> 129 {/* 戻るボタン */} 130 <button 131 onClick={() => { 132 backButton(); 133 }} 134 > 135 戻る 136 </button> 137 </div> 138 </> 139 ); 140}

備考

Diff

1- anser 2+ answer

投稿2021/09/03 09:13

k4a

総合スコア983

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

panda33

2021/09/03 12:08

k4aさん 非常に丁寧なご回答をいただき、誠にありがとうございます! いただいたコードで実行できたのはもちろん、ご解説いただいたおかげで理解もすることができました。 useStateの更新タイミングやstateの使い方も、とても勉強になりました。 丸2日くらいこの部分にはまっていたので、大変助かりました!ありがとうございます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問