title: How to detect a sequence of button press with gamepad api
body: <p>I am trying to use the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API" rel="nofollow noreferrer">Gamepad API</a> in react to detect a sequence of button or key presses such as the konami code. I found a blog post that shows how to detect when a gamepad is connected and I can do that as well as handle a state change however I'm stuck when it comes to detecting a sequence. The reason is that there is no 'onKeyUp' type of event and if a button is held it will emit an event every 1/60th of a second (because I am using requestAnimationFrame). I made a useGamepads hook following the blog and I have a Controller component where I am trying to detect the sequence.</p>
  <pre class="lang-js prettyprint-override"><code>export default function useGamepads(callback) {
    const gamepads = useRef({});
    const requestRef = useRef();
  
    var haveEvents = &quot;ongamepadconnected&quot; in window;
  
    const addGamepad = (gamepad) =&gt; {
  
      // console.log('gamepad: ', gamepad);
  
      gamepads.current = {
        ...gamepads.current,
        [gamepad.index]: {
          id: gamepad.id,
          axes: gamepad.axes,
          buttons: gamepad.buttons,
          connected: gamepad.connected,
          mapping: gamepad.mapping,
          index: gamepad.index,
          vibrationActuator: gamepad.vibrationActuator,
        }
      };
  
      callback(gamepads.current);
    };
  
    const connectGamepadHandler = (e) =&gt; {
      addGamepad(e.gamepad);
    };
  
    const scanGamepads = () =&gt; {
      // Grab gamepads from browser API
      const detectedGamepads = navigator.getGamepads
        ? navigator.getGamepads()
        : navigator.webkitGetGamepads
        ? navigator.webkitGetGamepads()
        : [];
  
      // Loop through all detected controllers and add if not already in state
      for (let i = 0; i &lt; detectedGamepads.length; i++) {
        if (detectedGamepads[i]) {
          addGamepad(detectedGamepads[i]);
        }
      }
    };
  
    // Add event listener for gamepad connecting
    useEffect(() =&gt; {
      window.addEventListener(&quot;gamepadconnected&quot;, connectGamepadHandler);
      return window.removeEventListener(&quot;gamepadconnected&quot;, connectGamepadHandler);
    }, []);
  
    // Update each gamepad's status on each &quot;tick&quot;
    const animate = (time) =&gt; {
      if (!haveEvents) scanGamepads();
      requestRef.current = requestAnimationFrame(animate);
    };
  
    useEffect(() =&gt; {
      requestRef.current = requestAnimationFrame(animate);
      return () =&gt; cancelAnimationFrame(requestRef.current);
    }, []);
  
    return gamepads.current;
  }
  </code></pre>
  <pre class="lang-js prettyprint-override"><code>const Controller = ({
    activeColor = &quot;#2F80ED&quot;,
    inactiveColor = &quot;#E0E0E0&quot;,
    showController = true,
    showControllerName = true,
    showLastControllerUpdate = true,
    onKonamiUnlocked = () =&gt; {},
  }) =&gt; {
    
    const [gamepad, setGamepad] = useState({});
    const [gamepads, setGamepads] = useState(null);
  
    const [lastControllerUpdate, setLastControllerUpdate] = useState({});
    const [controllerName, setControllerName] = useState('');
  
    // konami
    const konamiCodeSequence = ['directionUp', 'directionUp', 'directionDown', 'directionDown', 'directionLeft', 'directionRight', 'directionLeft', 'directionRight', 'buttonDown', 'buttonRight'];
    const [sequence, setSequence] = useState([]);
    const [konamiUnlocked, setKonamiUnlocked] = useState(false);
  
      
    useGamepads((gp) =&gt; setGamepads(gp));
  
    const debouncedSetSequence = debounce(setSequence, 750);
    const throttledSetSequence = throttle(setSequence, 750);
  
    useEffect(() =&gt; {
      if (!lastControllerUpdate) return;
      throttledSetSequence(prev =&gt; {
        return [...prev, lastControllerUpdate];
      });
    }, [lastControllerUpdate]); // gamepad
  
    
  
    const calcDirectionVertical = (axe) =&gt; {
      // Up
      if (axe &lt; -0.2) {
        return &quot;up&quot;;
      }
      // Down
      if (axe &gt; 0.2) {
        return &quot;down&quot;;
      }
    };
  
    const calcDirectionHorizontal = (axe) =&gt; {
      // Left
      if (axe &lt; -0.2) {
        return &quot;left&quot;;
      }
      // Right
      if (axe &gt; 0.2) {
        return &quot;right&quot;;
      }
    };
  
    const createTransform = (direction) =&gt; {
      switch (direction) {
        case &quot;up&quot;:
          return &quot;translateY(-10px)&quot;;
        case &quot;down&quot;:
          return &quot;translateY(10px)&quot;;
        case &quot;left&quot;:
          return &quot;translateX(-10px)&quot;;
        case &quot;right&quot;:
          return &quot;translateX(10px)&quot;;
  
        default:
          return &quot;&quot;;
      }
    };
  
    const onGamepadUpdate = (newGamePadState) =&gt; {
      for (const [key, value] of Object.entries(newGamePadState)) {
        if (typeof value === &quot;boolean&quot; &amp;&amp; value === true) {
          const newVal = { 
            id: Math.random().toString(36).substr(2, 4),
            val: key.toString(),
          };
          // console.log('newVal: ', newVal);
          setLastControllerUpdate(newVal); // key.toString());
        }
      }
    };
  
    const throttledGamepadUpdate = throttle(onGamepadUpdate, 1000);
    const debouncedGamepadUpdate = debounce(onGamepadUpdate, 1000);
  
    useEffect(() =&gt; {
  
      if (gamepads &amp;&amp; gamepads.length !== 0) {
  
        if (controllerName === '') {
          setControllerName(gamepads[0].id);
        }
  
        const newGamePadState = {
          directionUp: gamepads[0].buttons[12].pressed,
          directionDown: gamepads[0].buttons[13].pressed,
          directionLeft: gamepads[0].buttons[14].pressed,
          directionRight: gamepads[0].buttons[15].pressed,
          buttonDown: gamepads[0].buttons[0].pressed,
          buttonRight: gamepads[0].buttons[1].pressed,
          buttonLeft: gamepads[0].buttons[2].pressed,
          buttonUp: gamepads[0].buttons[3].pressed,
          buttonX: gamepads[0].buttons[16].pressed,
          // top of controller
          buttonLT: gamepads[0].buttons[6].pressed,
          buttonLB: gamepads[0].buttons[4].pressed,
          buttonRT: gamepads[0].buttons[7].pressed,
          buttonRB: gamepads[0].buttons[5].pressed,
  
          select: gamepads[0].buttons[8].pressed,
          start: gamepads[0].buttons[9].pressed,
          analogLeft: gamepads[0].axes[0] &gt; 0.3 || gamepads[0].axes[0] &lt; -0.3 || gamepads[0].axes[1] &gt; 0.3 || gamepads[0].axes[1] &lt; -0.3,
          analogRight: gamepads[0].axes[2] &gt; 0.3 || gamepads[0].axes[2] &lt; -0.3 || gamepads[0].axes[3] &gt; 0.3 || gamepads[0].axes[3] &lt; -0.3,
          analogLeftDirection: [
            calcDirectionHorizontal(gamepads[0].axes[0]),
            calcDirectionVertical(gamepads[0].axes[1])
          ],
          analogRightDirection: [
            calcDirectionHorizontal(gamepads[0].axes[2]),
            calcDirectionVertical(gamepads[0].axes[3])
          ],
          
        };
  
        // throttle and debounce do not seem to work...
        throttledGamepadUpdate(newGamePadState);
        // debouncedGamepadUpdate(newGamePadState);
        // onGamepadUpdate(newGamePadState);
        
  
        setGamepad({ ...newGamePadState });
      }
    }, [gamepads]);
  
    const {
      directionUp,
      directionRight,
      directionDown,
      directionLeft,
      select,
      start,
      buttonUp,
      buttonRight,
      buttonDown,
      buttonLeft,
      analogLeft,
      analogLeftDirection,
      analogRight,
      analogRightDirection,
    } = gamepad;
  
    return (
      &lt;div&gt;
        { showController &amp;&amp; 
          (
            &lt;svg width={288} height={144} viewBox=&quot;0 0 1280 819&quot; fill=&quot;none&quot; &gt;
              &lt;path
                className=&quot;background&quot;
                d=&quot;M209.5 7.246c11.7-2.7 26.5-5.2 38.5-6.6 12.5-1.4 38.5-.4 49 1.8 19.7 4.3 31.2 10.6 43.7 24.1 7.8 8.4 21.9 28.7 25.2 36.4 4.4 10.1 12.6 47.8 12.6 58.3v3.1h522v-3.1c0-5.2 4.8-32.2 7.6-43 3.5-13.1 6-18.6 13.5-29.9 12-17.9 23.6-30.5 33.3-36.2 6.4-3.7 19-8.1 29.2-10.1 11-2.2 40.4-2.5 54.4-.5 26.1 3.6 47.3 9.1 61 15.8 21 10.2 31.8 27.5 41.4 66 1.9 7.6 4 16.3 4.6 19.4l1.1 5.5 11.2 8c29 20.4 53.9 42.9 63.3 57.1 11.4 17.1 20.1 37.4 28.8 67.5 7.1 24.6 7.5 27.6 17.5 138.3 9.3 101.8 11.5 142.5 11.6 213 0 54.6-1.2 87.9-4 110.6-3.5 27.8-13.4 49.3-31.2 68-23.4 24.5-47.6 38.4-78.6 45.1-14.5 3.1-41.5 3.1-53 0-16.6-4.5-33.9-14.7-51.7-30.5-24.5-21.7-42.3-49.1-72.6-111.7-18.2-37.4-19.9-40.6-26.2-47.5-3.1-3.3-8-9.3-10.9-13.2l-5.4-7.3-10.2 8.3c-23.1 18.7-34.4 24.2-60.9 29.8-12.4 2.6-36.9 3.1-48.8 1-27.3-4.8-51.2-13.8-71-26.9-17.2-11.4-27.6-24.6-41.3-52.4l-7.2-14.6H573l-7.2 14.6c-13.7 27.8-24.1 41-41.3 52.4-20.1 13.2-43.7 22.1-71 26.9-11.9 2.1-36.4 1.6-48.8-1-26.5-5.6-37.8-11.1-60.9-29.8l-10.2-8.3-5.4 7.3c-3 3.9-8 10.1-11.3 13.7-4 4.4-7.6 9.9-11.1 17-2.8 5.8-10.8 22-17.6 36-28.5 58.3-47.1 86.1-71.4 107.1-17.8 15.4-33.8 24.7-50.1 29.1-11.4 3.1-38.5 3.1-52.9 0-31-6.7-55.2-20.6-78.6-45.1-17.8-18.7-27.7-40.2-31.2-68-2.8-22.7-4-56-4-110.6.1-70.4 2.3-111.1 11.6-213 10.2-112.6 10-111.3 15.9-132.9 8-29.2 17-51.6 27.4-68.6 10-16.2 33.5-38 65.4-60.8 6.4-4.5 11.7-8.4 11.8-8.5.2-.1 1.7-6.8 3.4-14.7 6.1-27.9 16.2-53.4 24.5-62.2 11.4-12 24.5-18.4 49.5-24.2z&quot;
                fill=&quot;#C4C4C4&quot;
              /&gt;
              &lt;path
                className=&quot;direction_up&quot;
                d=&quot;M269 165h-77v56c9.333 11.333 30 34 38 34s29.333-22.667 39-34v-56z&quot;
                fill={directionUp ? activeColor : inactiveColor}
              /&gt;
              &lt;path
                className=&quot;direction_right&quot;
                d=&quot;M341 240v77h-56c-11.333-9.333-34-30-34-38s22.667-29.333 34-39h56z&quot;
                fill={directionRight ? activeColor : inactiveColor}
              /&gt;
              &lt;path
                className=&quot;direction_down&quot;
                d=&quot;M269 392h-77v-56c9.333-11.333 30-34 38-34s29.333 22.667 39 34v56z&quot;
                fill={directionDown ? activeColor : inactiveColor}
              /&gt;
              &lt;path
                className=&quot;direction_left&quot;
                d=&quot;M119 240v77h56c11.333-9.333 34-30 34-38s-22.667-29.333-34-39h-56z&quot;
                fill={directionLeft ? activeColor : inactiveColor}
              /&gt;
              &lt;path
                className=&quot;select&quot;
                fill={select ? activeColor : inactiveColor}
                d=&quot;M471 262h75v47h-75z&quot;
              /&gt;
              &lt;path
                className=&quot;start&quot;
                d=&quot;M728 309v-49l72 23-72 26z&quot;
                fill={start ? activeColor : inactiveColor}
              /&gt;
              &lt;circle
                className=&quot;button_up&quot;
                cx={1050.5}
                cy={183.5}
                r={47.5}
                fill={buttonUp ? activeColor : inactiveColor}
              /&gt;
              &lt;circle
                className=&quot;button_right&quot;
                cx={1162.5}
                cy={283.5}
                r={47.5}
                fill={buttonRight ? activeColor : inactiveColor}
              /&gt;
              &lt;circle
                className=&quot;button_down&quot;
                cx={1050.5}
                cy={383.5}
                r={47.5}
                fill={buttonDown ? activeColor : inactiveColor}
              /&gt;
              &lt;circle
                className=&quot;button_left&quot;
                cx={935.5}
                cy={283.5}
                r={47.5}
                fill={buttonLeft ? activeColor : inactiveColor}
              /&gt;
              &lt;circle
                className=&quot;analog_left&quot;
                cx={429}
                cy={511}
                r={93}
                fill={analogLeft ? activeColor : inactiveColor}
                style={{
                  position: &quot;relative&quot;,
                  transition: &quot;transform 200ms ease-out&quot;,
                  transform: analogLeftDirection &amp;&amp; analogLeftDirection.length &gt; 0 ? `${createTransform(analogLeftDirection[0])} ${createTransform(analogLeftDirection[1])}` : &quot;&quot;,
                }}
              /&gt;
              &lt;circle
                className=&quot;analog_right&quot;
                cx={843}
                cy={511}
                r={93}
                fill={analogRight ? activeColor : inactiveColor}
                style={{
                  position: &quot;relative&quot;,
                  transition: &quot;transform 200ms ease-out&quot;,
                  transform: analogRightDirection &amp;&amp; analogRightDirection.length &gt; 0 ? `${createTransform(analogRightDirection[0])} ${createTransform(analogRightDirection[1])}` : &quot;&quot;,
                }}
              /&gt;
            &lt;/svg&gt;
          )
        }
      &lt;/div&gt;
    );
  };
  
  export default Controller;
  </code></pre>