skip to Main Content

This is an onscreen keyboard component.

I don’t feel like rewriting all the properties for a single key button every time because that would make the code look ugly, so I opted for an extra component that I treat as a "key button shorthand", problem is every time I press a key I notice the rest of the keys also rerender because of the state change, what’s an alternative solution to prevent this?

function Keyboard({ setState }: { setState: React.Dispatch<React.SetStateAction<string>> }) {
  let keyInterval = useRef<NodeJS.Timeout>()
  let keyTimeout = useRef<NodeJS.Timeout>()

  function keyDownBehaviour(action: () => void) {
    action()
    keyTimeout.current = setTimeout(() => {
      keyInterval.current = setInterval(() => {
        action()
      }, 50)
    }, 500)
  }

  function keyUpBehaviour() {
    clearInterval(keyInterval.current)
    clearTimeout(keyTimeout.current)
  }

  function Key({ text }: { text: string }) {
    return (
      <Button
        className="text-lg p-0 w-12"
        variant="outline"
        onMouseDown={() => keyDownBehaviour(() => setState(prevAnswer => prevAnswer + text))}
        onMouseUp={keyUpBehaviour}
        onMouseLeave={keyUpBehaviour}
      >
        {text}
      </Button>
    )
  }

  return (
    <div className="flex flex-col mt-2 gap-2 w-auto">
      <div className="flex gap-[0.2rem] sm:gap-2 w-auto">
        <Key text="b" />
        <Key text="a" />
      </div>
    </div>
  )
}

3

Answers


  1. You should not define components inside of your component but explicitly make it its own component. This way you can memoize the component.

    From the react documentation: "memo lets you skip re-rendering a component when its props are unchanged."

    In your case you can use it like:

    import { memo } from 'react' 
    
    const Key = memo(function Key({ text, setState }: { text: string, setState: React.Dispatch<React.SetStateAction<string>> }) {
      let keyInterval = useRef<NodeJS.Timeout>()
      let keyTimeout = useRef<NodeJS.Timeout>()
    
      function keyDownBehaviour(action: () => void) {
        action()
        keyTimeout.current = setTimeout(() => {
          keyInterval.current = setInterval(() => {
            action()
          }, 50)
        }, 500)
      }
    
      function keyUpBehaviour() {
        clearInterval(keyInterval.current)
        clearTimeout(keyTimeout.current)
      }
    
      return (
          <Button
            className="text-lg p-0 w-12"
            variant="outline"
            onMouseDown={() => keyDownBehaviour(() => setState(prevAnswer => prevAnswer + text))}
            onMouseUp={keyUpBehaviour}
            onMouseLeave={keyUpBehaviour}
          >
            {text}
        </Button>
      )
    
    })
    

    You can then just use it like

    return (
        <div className="flex flex-col mt-2 gap-2 w-auto">
          <div className="flex gap-[0.2rem] sm:gap-2 w-auto">
            <Key text="b" setstate={setState} />
            <Key text="a" setstate={setState} />
          </div>
        </div>
      )
    

    If the other keys are still rendering you might need to add a custom comparison function

    // ...
    
            {text}
        </Button>
      )
    
    }, (oldProps, newProps) => {
      // Only check if the text props changes
      // if not, react considers it as the same component and won't re-render
      return oldProps.text === newProps.text
    })
    
    Login or Signup to reply.
  2. You can prevent unnecessary rerenders in react using multiple hooks such as useCallback, useMemo, useLayOutEffect, useEffect but the most suitable method for the example you privided is React.memo which memoizes the rendered output of the component based on it’s props preventing unnecessary renders when props remains same.

    import React, { memo } from 'react';
    
    function Key({ text, setState }: { text: string, setState: 
    React.Dispatch<React.SetStateAction<string>> }) {
      const keyDownBehaviour = () => {
        setState((prevAnswer) => prevAnswer + text);
      };
    
      return (
        <Button
          className="text-lg p-0 w-12"
          variant="outline"
          onMouseDown={keyDownBehaviour}
          onMouseUp={() => {}}
          onMouseLeave={() => {}}
        >
          {text}
        </Button>
      );
    }
    
    // Memoize the Key component to prevent unnecessary rerenders
    const MemoizedKey = memo(Key);
    
    function Keyboard({ setState }: { setState: 
    React.Dispatch<React.SetStateAction<string>> }) {
      return (
        <div className="flex flex-col mt-2 gap-2 w-auto">
          <div className="flex gap-[0.2rem] sm:gap-2 w-auto">
            <MemoizedKey text="b" setState={setState} />
            <MemoizedKey text="a" setState={setState} />
          </div>
        </div>
      );
    }
    
    Login or Signup to reply.
  3. React is a super simple construct. Your App is a "tree of components" where the top component and is the only component without a parent.

    Components (which are simply JS functions) will Render (i.e. the function will be executed) under only 2 cases:

    • a state variable of the component itself is modified by the "set state function"
    • the component’s parent renders

    Since your Keyboard component is being passed down a "set state function" of its parent (setState), and Keyboard calls that function, then the Keyboard is changing a state value in its parent…

    ….which will cause the parent to re-render…

    ….which will cause ALL of the parent’s children to re-render…

    …which will cause all of Keyboard’s children to re-render…

    The question is: do you actually want (or need) to have Keyboard updating the state of Keyboard’s parent? Or is there a cleaner/better way to communicate the event press without updating state in the parent and causing a cascading of renders?

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search