Home > Enterprise >  Sharing state between event handlers in React
Sharing state between event handlers in React

Time:12-27

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)

CodePudding user response:

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;
  • Related