-1

The following code searches elements by text. It also sets a selectedElement (the first element in elements).

import { useEffect, useState, ChangeEvent } from "react";

function App() {
  const [inputValue, setInputValue] = useState("Initial value");
  const [elements, setElements] = useState<HTMLElement[]>([]);
  const [selectedElement, setSelectedElement] = useState<HTMLElement | null>(
    null
  );

  function findElementsByText(selectors: string, text: string) {
    if (text === "") return [];

    const regex = new RegExp(text);
    const elements = Array.from(
      document.querySelectorAll<HTMLElement>(selectors)
    );

    return elements.filter((element) => {
      return element.textContent?.match(regex);
    });
  }

  function handleInputChange(event: ChangeEvent<HTMLInputElement>) {
    const selectors = "abbr";
    const { value } = event.target as HTMLInputElement;
    const foundElements = findElementsByText(selectors, value);
    const foundSelectedElement = foundElements[0] || null;
    setInputValue(value);
    setElements(foundElements);
    console.log("selectedElement from handleInputChange", foundSelectedElement);
    setSelectedElement(foundSelectedElement);
  }

  function isCommand(event: KeyboardEvent) {
    return event.ctrlKey || event.key === "Enter" || event.key === "Escape";
  }

  function handleDocumentKeyDown(event: any) {
    if (!isCommand(event)) return;

    if (event.ctrlKey && event.key === "]") {
      console.log("selectedElement from handleDocumentKeyDown", selectedElement);
    }
  }

  useEffect(() => {
    document.addEventListener("keydown", handleDocumentKeyDown);

    return () => {
      document.removeEventListener("keydown", handleDocumentKeyDown);
    };
  }, []);

  return (
    <div id="keys">
      <input type="text" onChange={handleInputChange} value={inputValue} />
      <span id="count">
        {selectedElement ? elements.indexOf(selectedElement) + 1 : 0}/
        {elements.length}
      </span>
      <br/>
      <abbr>a</abbr>
      <abbr>b</abbr>
      <abbr>c</abbr>
    </div>
  );
}

export default App;

I want Ctrl + ] to set selectedElement to the next element. To do that, I have to be able to access selectedElement inside handleDocumentKeyDown.

But if, for example, I type a in the input (triggering handleInputChange and setSelectedElement()), then I press Ctrl + ] (triggering handleDocumentKeyDown), selectedElement will be null, even though foundSelectedElement is <abbr>a</abbr>.

I thought when I pressed Ctrl + ], I'd already be in the "next render" and therefore I'd be able to access the newest value of selectedElement in handleDocumentKeyDown. But it wasn't the case.

How to change the code so that selectedElement isn't null in handleDocumentKeyDown, and instead has the HTML element set by setSelectedElement() in handleInputChange?

Live code:

Edit useState same value rerender (forked)

alexchenco
  • 53,565
  • 76
  • 241
  • 413
  • Would you please actually _read_ e.g. https://stackoverflow.com/questions/54069253/the-usestate-set-method-is-not-reflecting-a-change-immediately - there's only so many times you can possibly be surprised by such a straightforward concept as a _closure_. – jonrsharpe Dec 26 '22 at 16:33
  • @jonrsharpe I thought when I pressed `Ctrl + ]`, I'd already be in the "next render" and therefore I'd be able to access the newest value of `selectedElement` in `handleDocumentKeyDown` (since it's a state). – alexchenco Dec 26 '22 at 17:00
  • 1
    Again, it's **closed over**. You've very carefully added the event listener only once, when the component first renders, with your `useEffect`. – jonrsharpe Dec 26 '22 at 17:12

1 Answers1

1

You can solve your problem using useRef hook. Here is my solution (In this solution, I used a new ref variable selectedElementRef.):

import { useEffect, useState, useRef, ChangeEvent } from "react";

function App() {
  const [inputValue, setInputValue] = useState("Initial value");
  const [elements, setElements] = useState<HTMLElement[]>([]);
  const [selectedElement, setSelectedElement] = useState<HTMLElement | null>(
    null
  );
  const selectedElementRef = useRef(selectedElement)
  selectedElementRef.current = selectedElement

  function findElementsByText(selectors: string, text: string) {
    if (text === "") return [];

    const regex = new RegExp(text);
    const elements = Array.from(
      document.querySelectorAll<HTMLElement>(selectors)
    );

    console.log(elements)

    return elements.filter((element) => {
      return element.textContent?.match(regex);
    });
  }

  function handleInputChange(event: ChangeEvent<HTMLInputElement>) {
    const selectors = "abbr";
    const { value } = event.target as HTMLInputElement;
    const foundElements = findElementsByText(selectors, value);
    const foundSelectedElement = foundElements[0] || null;
    setInputValue(value);
    setElements(foundElements);
    console.log("selectedElement from handleInputChange", foundSelectedElement);
    setSelectedElement(foundSelectedElement);
  }

  function isCommand(event: KeyboardEvent) {
    return event.ctrlKey || event.key === "Enter" || event.key === "Escape";
  }

  function handleDocumentKeyDown(event: any) {
    if (!isCommand(event)) return;

    if (event.ctrlKey && event.key === "]") {
      console.log(
        "selectedElement from handleDocumentKeyDown",
        selectedElementRef.current
      );
    }
  }

  useEffect(() => {
    document.addEventListener("keydown", handleDocumentKeyDown);

    return () => {
      document.removeEventListener("keydown", handleDocumentKeyDown);
    };
  }, []);

  return (
    <div id="keys">
      <input type="text" onChange={handleInputChange} value={inputValue} />
      <span id="count">
        {selectedElement ? elements.indexOf(selectedElement) + 1 : 0}/
        {elements.length}
      </span>
      <br />
      <abbr>a</abbr>
      <abbr>b</abbr>
      <abbr>c</abbr>
    </div>
  );
}

export default App;
talent-jsdev
  • 656
  • 4
  • 14
  • 1
    It's related to javascript closure. We need to use the reference to access the global state variable in the closure in React. For that, I created and used a selectedElementRef variable. – talent-jsdev Dec 27 '22 at 07:50