gcr
gcr

Reputation: 463

Keydown event listener causes lagging, hanging, DOM thrashing in ReactJS

I'm running a site locally and deployed with an 80 page modal in the React framework. I use state to move through the pages in the modal using the [currentStep, setCurrentStep] hooks. setCurrentStep can be invoked three ways- through back and next click button, through an input box by entering a page number and then hitting enter, and through the left and right keyboard keys. It's only the left and right keys that give me a problem. After several times clicking them (with pauses in between) the page becomes unresopnsive.

I thought maybe it had something to do with listeners, blocking or too many unfinished calls, and I thought I could maybe find answers with features of dev tools like memory and event listener but these didn't help me. I noted that the input box option also used a similar event listener so I thought there could be an interaction somehow, so I changed it to a keyup listener and still go the same effects.

Then I noticed as I clicked an arrow, the DOM started thrashing: mounting and unmounting a few sibling elements back and forth through two or three pages worth, which I can pick out despite the speed. Doing this in from the beginning I noticed the first time, it was normal. The second time it thrashed just a few times, and then it got higher and higher, almost like it was an n! function.

What's interesting is that the other two methods (button and input) call the exact same setCurrentStep and nothing else. There must be some side effect happening or some difference somewhere, that makes think there needs to be a mount and unmount, despite them going through the same narrow pathways. I must be missing something. I am pretty sure it's not a data type issue. The next and back functions handle the incrementing and decrementing.

import React, { ReactNode, useState } from "react";
import { QuestionsObject } from './QuestionData/raw/QuestionCompiler'
import { ViewContainer } from './Views/ViewContainer'

const MultiStepAssessmentForm = (props) =>
{
  const [currentStep, setCurrentStep] = useState(0);

  let questionsArray = Object.keys(QuestionsObject)
  let qIndex = questionsArray[currentStep]

  const handleKeyDown = function (e)
  {
    if (e.code == 'NumpadEnter'){
      setCurrentStep(parseInt(e.target.value))
      e.target.value = ''
    }
  }

  const next = () => { setCurrentStep(currentStep + 1) };
  const back = () => { setCurrentStep(currentStep - 1); };

  document.body.addEventListener('keydown', (e) =>
  {
    if (e.code == 'ArrowRight') next()
    if (e.code == 'ArrowLeft' && currentStep > 0) back()
  })


  const formProps = {
    data: formData,
    handleInputBoxChange: handleInputBoxChange,
    handleKeyValueCHange: handleKeyValueChange,
    currentStep: currentStep,
    next: next,
    back: back,
  }

  function renderQuestions()
  {
    return (
      <div className="temp Dev Div">
        <input type="text" placeholder="Go to page X" onKeyDown={(e) => { handleKeyDown(e) }} />
        <ViewContainer formProps={formProps} question={QuestionsObject[qIndex]} />
      </div>
    )
  }

  return (
    <> {renderQuestions()}</>
  )

};
export default MultiStepAssessmentForm;

My best guess is that it is because I am attaching the listener to the body and that messes with React. It's one of two differences I see and also makes some intuitive sense. I don't know where else to attach the listener to though, as page elements could be null when it is run, and maybe this is not even the problem. The other difference is I thought maybe it was declared as an anonymous function, so it can't get 'garbage collected' by js or something, if that's a thing, so I decided to make it a named function as best I could. I still had to wrap the named function in an anonymous one, which I'm getting used to, becaues otherwise I didn't know how to pass it the Event object e, which I need for the keycode. This didn't solve it though. Same issues.

  function buttonListener(e: KeyboardEvent)
  {
    {
      if (e.code == 'ArrowRight') { next() }
      else if (e.code == 'ArrowLeft' && currentStep > 0) { back() }
    }
  }
  document.body.addEventListener('keydown', (e) => { buttonListener(e) })

Does anyone have any ideas? React thinks there needs to be an update, but not the first few times. What changes? And why no thrashing the first few times? I'm trying to learn more about both events and about react so this is a useful problem.

Upvotes: 0

Views: 1349

Answers (1)

user5125954
user5125954

Reputation:

You should add event listeners only once and remove listener after modals gone. For that, We can add listeners after React components mounted by using a useEffect hook. If you add listeners outside of useEffect hook, It will eventually add anonymous event listeners every render.

const keyboardEventListener = (e) =>
  {
    if (e.code == 'ArrowRight') next()
    if (e.code == 'ArrowLeft' && currentStep > 0) back()
  }

useEffect(() =>
  {
    document.body.addEventListener('keydown', keyboardEventListener);
    return () =>
      {
        document.body.removeEventListener('keydown', keyboardEventListener);
      }
  }, [])

Upvotes: 2

Related Questions