Preact: Accessing component methods from a window keyboard listener

Created on 28 Apr 2020  路  5Comments  路  Source: preactjs/preact

Hello, I'm having issues in functional components when using window event handlers

  • I have 2 states, an array: [string] and an index: number
  • Inside a useEffect that run only first time the component renders, two things are happening

    • The array is hydrating

    • A window keydown event starts

  • I have a function 'next()' which update the index (index + 1) based on the array length
  • I tried with useCallback and regular arrow functions

Reproduction

https://stackblitz.com/edit/js-5srjzd?file=index.js

Expected Behavior

The next function should access to the actual version of the array

Actual Behavior

When I use the right arrow from the keyboard

  • The window event triggers the next function
  • Inside the next function the array is empty

If I use the UI button to call the next function, it works as expected

All 5 comments

Hi,
I looked into it, and it is not a bug in the preact.

The problem is that the event handler you setup in useEffect is called only once using a function called navigation that was defined in the context of function it was defined. In other words, you are declaring const navigate each time the pure component renders, but it is used only in the first render - in that first render, items are an empty array [], and that's why your code doesn't work.

I created a dirty solution in this sandbox to show you how to overcome the problem - you will probably want to create a more sophisticated solution, but to understand the problem, it will be enough. In this example, I refresh the global variable window.newNavigateFunc with new navigate function each time pure component renders. Keypress event handler is modified to always use window.newNavigateFunc which has access to current items variable.
https://stackblitz.com/edit/js-zbbznk?file=index.js

Another solution could be calling useState once again in the next function to retrieve current items.

const next = () => {
    const [items] = useState([]);
    const num = idx === items.length - 1 ? 0 : idx + 1;
    console.log('next num', num)
    setIdx(num);
};

It is usually recommended to use useState at the beginning of pure component exclusively, but in this simple scenario, it shouldn't create any problems.

It is usually recommended to use useState at the beginning of pure component exclusively, but in this simple scenario, it shouldn't create any problems.

I would advise against this. Because of the way hooks work, the sequence of hook calls that happens when a component is rendered must always be the same. So you can't have hooks in if statements, loops or in a callback that may be called at some point in the future.

You're right about the cause though. The useEffect hook is capturing the next callback from the first render. To avoid issues like this, using an ESLint plugin to check for proper usage of hooks is recommended.

@aqrojo, something that may be helpful here is to make use of functional updates which allow a state updater to access the previous state. I had a stab at modifying your example: https://stackblitz.com/edit/js-udexcv?file=index.js

I'm closing this issue as it isn't a bug in Preact, but others are welcome to chime in with alternative solutions.

Hey @ondratra and @robertknight, thanks so much for your help, I usually works with react and I really wanted to testing how preact works with the use of hooks!

I was trying some ideas based on your examples and I think that I finally found the bug, basically the useEffect hook needs the dependency of the items array and it works like a charm!

By the way it has the same behavior using react
preact example
react example

I was trying some ideas based on your examples and I think that I finally found the bug, basically the useEffect hook needs the dependency of the items array and it works like a charm!

That's _almost_ right. You need to add the navigate callback as a dependency to useEffect, not the items variable. Your effects should declare their _direct_ dependencies, not their indirect ones. The reason is that otherwise, your code could break if you changed your navigate function to add (or remove) a dependency, but forgot to update the useEffect dependencies.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

paulkatich picture paulkatich  路  3Comments

k15a picture k15a  路  3Comments

SabirAmeen picture SabirAmeen  路  3Comments

mizchi picture mizchi  路  3Comments

marcosguti picture marcosguti  路  3Comments