I have a fairly simple case, I didn't think it would cause me problems. If Shift key is pressed I want to add a e.preventDefault() to my selectstart DOM event using React. Inoticed that the problem is that the isShiftDown doesn't update correctly. I don't understand why this is happening.
The isShiftDown isnside handleSelectStart function in Paragraph component is always false.
The same value inside App component toggle correctly.
All I have to do is prevent text selecting inside Paragraph if Shift is pressed. StackBlitz example may seem strange, but my real case much more expanded.
stackblitz example
// Paragraph
const Paragraph = ({ isShiftDown }) => {
  let paragraphRef;
  const handleSelectStart = e => {
    console.log('Paragraph => handleSelectStart => isShiftDown =', isShiftDown);
    if (isShiftDown) {
      e.preventDefault();
    }
  };
  React.useEffect(() => {
    paragraphRef.addEventListener('selectstart', handleSelectStart, false);
    return () => {
      paragraphRef.removeEventListener('selectstart', handleSelectStart);
    };
  }, []);
  return (
    <p ref={e => (paragraphRef = e)}>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    </p>
  );
};
// App
const App = () => {
  const [isShiftDown, setIsShiftDown] = React.useState(false);
  const handleKeyUp = e => {
    if (e.key === 'Shift' && isShiftDown) {
      setIsShiftDown(false);
    }
  };
  const handleKeyDown = e => {
    if (e.key === 'Shift' && !isShiftDown) {
      setIsShiftDown(true);
    }
  };
  React.useEffect(() => {
    document.addEventListener('keyup', handleKeyUp, false);
    document.addEventListener('keydown', handleKeyDown, false);
    return () => {
      document.removeEventListener('keyup', handleKeyUp, false);
      document.removeEventListener('keydown', handleKeyDown, false);
    };
  }, []);
  return (
    <div className="App">
      <Paragraph isShiftDown={isShiftDown} />
    </div>
  );
};
ReactDOM.render(
  <App />,
  document.getElementById("react")
);<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>First, your unbinding cannot work, due to the fact that every time the component is re-evaluated, the event handlers' variables point to a new function. You should save a reference to it inside useEffect or simply use useCallback when defining the handlers:
const handleKeyUp = useCallback(e => {
    if (e.key === "Shift" && setIsShiftDown) {
      setIsShiftDown(false);
    }
}, [setIsShiftDown])
Also, keyboard events should be binded at the window level object and not the document.
You should write it as follows:
import React, { useEffect, useCallback } from "react";
export default function Paragraph({ isShiftDown }) {
  let paragraphRef;
  const handleSelectStart = useCallback( <---------- WRAP WITH HOOK
    e => {
      console.log("Paragraph => handleSelectStart => isShiftDown =", isShiftDown)
      if (isShiftDown) {
        e.preventDefault()
      }
    }, [isShiftDown])
  useEffect(() => {
    const ref = paragraphRef;
    ref.addEventListener("selectstart", handleSelectStart);
    return () => {
      ref.removeEventListener("selectstart", handleSelectStart);
    };
  }, [isShiftDown]) // <---------- ADD DEPENDENCY
  return (
    <p ref={el => (paragraphRef = el)}>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    </p>
  )
}
Every time Paragraph component gets called from App.js, it basically calls the Paragraph function, which has its own scope, its own paragraphRef and so on. React can be very confusing if fundamentals are not well understood.
Knowing the above, you will know if a piece of code inside the component is running in the scope of the latest iteration of the component, or a previous one.
How to know? Reading the code carefully and step-by-step simulating the flow in your mind, creating a vision of how it will act on the next time it is called. Knowing JS fundementals and knowing React is key to proper brain-parser which case save a lot of debugging time. it's all about the brain-parser.
Since you did not specify any dependencies for the useEffect in Paragraph.js`, it will only run once, on the first instance, and that's it.
You must re-bind the events every time the component re-renders, so everything will work with the last "version" of the DOM, as you intend, and not previous renders:
useEffect(() => {
  ...
}, [isShiftDown]);
You don't really need to wrap the event callback with useCallback, but I would prefer to, in this scenario.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With