BOUNTY UPDATE: I closed this question mistakenly to early thinking this post solved my problem (you can see my iteration of code from that post at the bottom under edit). I opened another question where I asked the updated question not realizing I could reopen this. It has been made apparent directly accessing the dom in react is bad, and I am only interested in answers that actually address the issue I am having, I have changed the code to the following:
export default function Home() {
const [words, setWords] = useState([
"these",
"are",
"the",
"words",
"you",
"are",
"typing",
]);
const [currword, setCurrword] = useState(0);
const [currletter, setCurrletter] = useState(0);
const activeWord = useRef<HTMLDivElement>(null);
const activeLetter = useRef<HTMLSpanElement>(null);
useEffect(() => {
document.onkeydown = (e) => {
if (
words[currword].length == currletter &&
e.key != " " &&
e.key != "Backspace"
) {
let wordsCopy = [...words];
wordsCopy[currword] = wordsCopy[currword] + e.key;
setWords(wordsCopy);
//activeletter.current is null here, this is the issue
if (activeLetter.current) {
activeLetter.current.className = styles.wrongextra;
}
setCurrletter(currletter + 1);
}
//letters are not added, regular check
else {
if (e.key == words[currword][currletter]) {
if (activeLetter.current) {
activeLetter.current.className = styles.right;
}
} else {
if (activeLetter.current) {
activeLetter.current.className = styles.wrong;
}
}
setCurrletter(currletter + 1);
}
};
}, [currletter, currword, words, activeLetter, activeWord]);
return (
<div>
<div
className={styles.container}
>
{words.map((word, widx) => {
return (
<div
ref={widx == currword ? activeWord : null}
>
{word.split("").map((letter, lidx) => {
return (
<span
ref={
lidx == currletter && widx == currword
? activeLetter
: null
}
>
{letter}
</span>
);
})}
</div>
);
})}
</div>
</div>
);
}
I still have the same issue where I am able to access and change css styles by using the ref unless I add letters to a word in the array of words, in that case the letters are added to the words and rendered but they have no reference. I will accept any answer that provides a solution to this problem the way I am currently trying to solve it or offers a substantially better solution using a different way (ie not using refs at all), and provides a code sample in their answer.
ORIGINAL POST WITH EDIT ON THE BOTTOM:
I am building typing test app with similar functionality to monkeytype
I have an array of words set to words
in usestate that I am iterating through to keep track of which word the user is on and which letter of the word the user is on.
const [words, setWords] = useState(["these", "are", "the", "words"]);
I am using useref
to reference the letter of the word the user is on and be able to change the css property color to either red or green based on if it is the correct input.
If the user has typed in the entire word displayed (so all letters will be colored) and does not hit spacebar to move on to the next word nor backspace to go to the previous letter, but types more letters instead in the same word, I am adding those additional incorrect letters to the end of the word like so:
let wordsCopy = [...words];
wordsCopy[currword] = wordsCopy[currword] + e.key;
setWords(wordsCopy);
Any letters I add this way with useref I am not able to access via the same useref that works on letters that are natively in the word and not added. with code like this:
if (activeLetter.current) activeLetter.current.style.color = "red";
Here is the full code, however I took out the spacebar and backspace functionality as they are not relative, just to shorten the code. (widx = word index, lidx = letter index).
function App() {
const [words, setWords] = useState([
"these",
"are",
"the",
"words",
"you",
"are",
"typing",
]);
const [currword, setCurrword] = useState(0);
const [currletter, setCurrletter] = useState(0);
const activeWord = useRef(null);
const activeLetter = useRef(null);
useEffect(() => {
document.onkeydown = (e) => {
if (
words[currword].length == currletter &&
e.key != " " &&
e.key != "Backspace"
) {
let wordsCopy = [...words];
wordsCopy[currword] = wordsCopy[currword] + e.key;
setWords(wordsCopy);
// activeLetter.current is null here, color doesn't change
if (activeLetter.current) {
activeLetter.current.style.color = "red";
}
setCurrletter(currletter + 1);
} else {
if (e.key == words[currword][currletter]) {
if (activeLetter.current) {
activeLetter.current.style.color = "green";
}
} else {
if (activeLetter.current) {
activeLetter.current.style.color = "red";
}
}
setCurrletter(currletter + 1);
}
};
}, [currletter, currword, words]);
return (
<div>
{words.map((word, widx) => {
return (
<div ref={widx == currword ? activeWord : null}>
{word.split("").map((letter, lidx) => {
return (
<span
ref={
lidx == currletter && widx == currword ? activeLetter : null
}
>
{letter}
</span>
);
})}
</div>
);
})}
</div>
);
}
One solution I have thought of would be making a dictionary of all the words index and their original length and then some dynamic css style that if the letter index is larger than the original index then it immediately becomes red, ie:
style={lidx > ogword.length && widx == currword ? { color: "red" } : {}}
but thats more code than feels necessary while offering less control I feel, so a solution that would make activeLetter.current work would be much preferable. Or at least explanation why this is happening and clarification if the solution I am looking for is possible or not.
EDIT: I accepted the close of this question as I thought this post answered my question, however using ref this way results in the same issue where you cannot access any letters that where added with setWords
. My updated code that has the exact same error:
function App() {
const [words, setWords] = useState([
"these",
"are",
"the",
"words",
"you",
"are",
"typing",
]);
const [currword, setCurrword] = useState(0);
const [currletter, setCurrletter] = useState(0);
const activeWord = useRef(words.map(() => createRef()));
const activeLetter = useRef(words[currword].split("").map(() => createRef()));
useEffect(() => {
document.onkeydown = (e) => {
if (
words[currword].length == currletter &&
e.key != " " &&
e.key != "Backspace"
) {
let wordsCopy = [...words];
wordsCopy[currword] = wordsCopy[currword] + e.key;
setWords(wordsCopy);
setCurrletter(currletter + 1);
//same error here as before, is null here
if (activeLetter.current[currletter].current) {
activeLetter.current[currletter].current.style.color = "red";
}
} else {
if (e.key == words[currword][currletter]) {
if (activeLetter.current[currletter].current) {
activeLetter.current[currletter].current.style.color = "green";
}
} else {
if (activeLetter.current[currletter].current) {
activeLetter.current[currletter].current.style.color = "red";
}
}
setCurrletter(currletter + 1);
}
};
}, [currletter, currword, words, activeWord]);
return (
<div>
{words.map((word, widx) => {
return (
<div ref={activeWord.current[widx]}>
{word.split("").map((letter, lidx) => {
return (
<span
ref={widx == currword ? activeLetter.current[lidx] : null}
>
{letter}
</span>
);
})}
</div>
);
})}
</div>
);
}
export default App;
I have also tried moving around the setCurrletter(currletter + 1);
before and after I set the new word and where I am trying to access the styles of the letter. Where it is now at least allows for the letter to be added to the word however it does not get the correct ref object so the color is not changed.