5

React beginner here.

My code

import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useState, useEffect } from 'react';
import Mousetrap from 'mousetrap';

export default function Home() {
  const [count, setCount] = useState(0);

  const triggerSomething = () => {
    console.log(count);
  };

  useEffect(() => {
    Mousetrap.bind(['ctrl+s', 'command+s'], e => {
      e.preventDefault();
      triggerSomething();
    });

    return () => {
      Mousetrap.unbind(['ctrl+s', 'command+s']);
    };
  }, []);

  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <h1 className={styles.title}>count: {count}</h1>

        <p className={styles.description}>
          <button onClick={() => setCount(count + 1)}>increment</button>
          <br />
          <br />
          <button onClick={triggerSomething}>triggerSomething</button>
        </p>
      </main>
    </div>
  );
}

I'm having an issue when trying to trigger an event from Mousetrap. The count variable is not reactive when triggered from mousetrap but reactive when triggered from the button with onClick.

To replicate this bug you need to:

  1. click the increment button once
  2. click the triggerSomething button. The console should print out 1 (the current state of count)
  3. push command+s or ctrl+s to trigger the same method. The console prints out 0 (the state of count when the component loaded). That should print 1 (the current state).

What am I doing wrong? What pattern should I use here?

UPDATE: Stackblitz here

adrianthedev
  • 626
  • 1
  • 11
  • 21

2 Answers2

4

When you change the state, the component is re-rendered, i.e. the function is executed again, but the useState hook returns the updated counter this time. To use this updated value in your MouseTrap, you must create a new handler (and remove the old one). To achieve this, simply remove the dependency array of your useEffect call. It will then use the newly created triggerSomething function.

import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useState, useEffect } from 'react';
import Mousetrap from 'mousetrap';

export default function Home() {
  const [count, setCount] = useState(0);

  const triggerSomething = () => {
    console.log(count);
  };

  useEffect(() => {
    Mousetrap.bind(['ctrl+s', 'command+s'], e => {
      e.preventDefault();
      triggerSomething();
    });

    return () => {
      Mousetrap.unbind(['ctrl+s', 'command+s']);
    };
  }); // Notice that I removed the dependency array

  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <h1 className={styles.title}>count: {count}</h1>

        <p className={styles.description}>
          <button onClick={() => setCount(count + 1)}>increment</button>
          <br />
          <br />
          <button onClick={triggerSomething}>triggerSomething</button>
        </p>
      </main>
    </div>
  );
}
Tracer69
  • 1,050
  • 1
  • 11
  • 26
  • Got it! It just seemed a bit wasteful to bind & unbind that event every time the component rendered. Thank you! – adrianthedev Jun 30 '21 at 09:36
  • As I stated. I'm a newbie with React. – adrianthedev Jun 30 '21 at 09:36
  • or add [count] in dependency, I think React recommends adding dependencies if there are any dependent value than not using one. But I don't the way it needs to bind each time it re-renders. – Raj Jun 30 '21 at 09:47
  • @Rox The ``useEffect`` depends on the outer function ``triggerSomething`` which is recreated on every render. Hence there is no difference between including it in the dependencies or not. – Tracer69 Jun 30 '21 at 11:27
3

In the triggerSomething method you can use setCount(count => count + 1) and this should work.

The issue you have is when you don't put the triggerSomething as a dep in the useEffect, count would be the same as the initial state which is 0. but when passing a function counting on the previous value to setCount, you'll spare this issue.

Dvir Hazout
  • 251
  • 1
  • 3