React state not reactive when triggered from outside library

Viewed 310

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

2 Answers

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>
  );
}

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.

Related