0

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.

Caleb
  • 439
  • 8
  • 29
  • Take this hint `const elementsRef = useRef(data.map(() => createRef()));` – Normal Nov 09 '22 at 23:22
  • 1
    Hello! Have you considered the approach of maintaining a separate piece of state for the person's actual input and then using comparison within the render section to determine what the color of each rendered letter should be? Happy to churn out a snippet to illustrate such an approach. I imagine that this approach would have no need for refs, and will allow you to easily display results for previous typing tests so long as you previously saved the person's inputs for the typing test. – rexess Nov 16 '22 at 07:59
  • @rexessilfie I would love to see the code snippet for this if it solves the issue as right away I am not sure if you are maintaining the current words input, all input total by letters including spaces or not, etc. I believe I have found a different work around this morning I am still putting together, however I think the code is much longer than it has to be, I would love to see your snippet and then implement it myself, your solution sounds way better than my current one but my implementation of it without your example will probably be worse. If it works I will 100% accept your answer. – Caleb Nov 16 '22 at 18:33
  • Here is a snippet for you [link](https://codesandbox.io/s/throbbing-http-dcbn7v?file=/src/App.js:0-2314). I chose not to post as an answer since it technically does not address the usage of refs, and simplifies your `words` to be a string instead of an array. If this meets your needs kindly adapt your question and I can post it as an answer for the benefit of you and other people that might come here with a similar goal as yours. Otherwise I could look into a ref-specific approach, and/or one with an array of `words`. – rexess Nov 18 '22 at 00:12
  • When you say "typing". Is the whole functionality a input where the user write and then you are trying to show what he's writing and set a specific color to the last letter? – jircdeveloper Nov 18 '22 at 13:20

0 Answers0