Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Value of variable outside of useEffect hook has old data

What the code does: It's performing a DOM search based on what's typed in an input (it's searching elements by text). All this is happening in a React component.

import { useEffect, useReducer } from "react";
let elements: any[] = [];

const App = () => {
  const initialState = { keyEvent: {}, value: "Initial state" };
  const [state, updateState] = useReducer(
    (state: any, updates: any) => ({ ...state, ...updates }),
    initialState
  );

  function handleInputChange(event: any) {
    updateState({ value: event.target.value });
  }

  function isCommand(event: KeyboardEvent) {
    return event.ctrlKey;
  }

  function handleDocumentKeyDown(event: any) {
    if (isCommand(event)) {
      updateState({ keyEvent: event });
    }
  }

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

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

  useEffect(() => {
    const selectors = "button";

    const pattern = new RegExp(state.value === "" ? "^$" : state.value);

    elements = Array.from(document.querySelectorAll(selectors)).filter(
      (element) => {
        if (element.childNodes) {
          const nodeWithText = Array.from(element.childNodes).find(
            (childNode) => childNode.nodeType === Node.TEXT_NODE
          );

          if (nodeWithText) {
            // The delay won't happenn if you comment out this conditional statement:
            if (nodeWithText.textContent?.match(pattern)) {
              return element;
            }
          }
        }
      }
    );

    console.log('elements 1:', elements)
  }, [state]);

  console.log('elemets 2:', elements)

  return (
    <div>
      <input
        id="input"
        type="text"
        onChange={handleInputChange}
        value={state.value}
      />
      <div id="count">{elements.length}</div>
      <button>a</button>
      <button>b</button>
      <button>c</button>
    </div>
  );
};

export default App;

The problem: The value of elements outside of useEffect is the old data. For example, if you type a in the input, console.log('elements 1:', elements) will log 1, and console.log('elements 2:', elements) will log 0. Note: there are 3 buttons, and one of them has the text a.

The strange thing is that the problem doesn't happen if you comment out this if-statement:

// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
  return element;
}

In this case, if you type anything (since the pattern matching has been commented out), console.log('elements 1:', elements) and console.log('elements 2:', elements) will log 3. Note: there are 3 buttons.

Question: What could be the problem, and how to fix it? I want to render the current length of elements.

Live code:

Edit useState same value rerender (forked)

like image 916
alexchenco Avatar asked Sep 03 '25 17:09

alexchenco


1 Answers

It's happening because of the elements variable is not a state, so it's not reactive. Create a state for the elements:

  const [elements, setElements] = useState<HTMLButtonElement[]>([])

And use this state to handle the elements.

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

const App = () => {
  const initialState = { keyEvent: {}, value: "Initial state" };
  const [state, updateState] = useReducer(
    (state: any, updates: any) => ({ ...state, ...updates }),
    initialState
  );
  const [elements, setElements] = useState<HTMLButtonElement[]>([])

  function handleInputChange(event: any) {
    updateState({ value: event.target.value });
  }

  function isCommand(event: KeyboardEvent) {
    return event.ctrlKey;
  }

  function handleDocumentKeyDown(event: any) {
    if (isCommand(event)) {
      updateState({ keyEvent: event });
    }
  }

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

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

  useEffect(() => {
    const selectors = "button";

    const pattern = new RegExp(state.value === "" ? "^$" : state.value);

    let newElements = Array.from(document.querySelectorAll(selectors)).filter(
      (element) => {
        if (element.childNodes) {
          const nodeWithText = Array.from(element.childNodes).find(
            (childNode) => childNode.nodeType === Node.TEXT_NODE
          );

          if (nodeWithText) {
            // The delay won't happenn if you comment out this conditional statement:
            if (nodeWithText.textContent?.match(pattern)) {
              return element;
            }
          }
        }
      }
    );
    setElements(newElements)

    console.log("elements 1:", elements?.length);
  }, [state]);

  console.log("elemets 2:", elements?.length);

  return (
    <div>
      <input
        id="input"
        type="text"
        onChange={handleInputChange}
        value={state.value}
      />
      <div id="count">{elements?.length}</div>
      <button>a</button>
      <button>b</button>
      <button>c</button>
    </div>
  );
};

export default App;
like image 72
Briuor Avatar answered Sep 05 '25 07:09

Briuor