1

I'm working on a component for a chrome extension that needs to listen for messages passed from other parts of the extension and update the display based on those messages.

A simplified version of what I thought should work is below but it does not do what I would expect.

This snippet successfully receives the message and calls the increment function, but never increments passed 1, with the log just repeating Incrementing 0 -> 1

// app.jsx
import React from 'react'
import { createRoot } from "react-dom/client";

function App() {
    const [ counter, setCounter ] = React.useState(0)

    function increment() {
        console.log("Incrementing ", counter, " -> ", (counter + 1));
        setCounter(counter + 1)
    }

    React.useEffect(() => {
        chrome.runtime.onMessage.addListener(message => increment())
    }, [ ])

    return <h1>Counter: {counter}</h1>
}

createRoot(document.getElementById('root')).render(<App />)

After some experimentation I discovered that if I set an intermediary value, and then update the counter via a seperate effect, as in the snippet below, it works exactly as I would expect.

// app.jsx
import React from 'react'
import { createRoot } from "react-dom/client";

function App() {
    const [ counter, setCounter ] = React.useState(0)
    const [ message, setMessage ] = React.useState()

    function increment() {
        console.log("Incrementing ", counter, " -> ", (counter + 1));
        setCounter(counter + 1)
    }

    React.useEffect(() => {
        chrome.runtime.onMessage.addListener(message => setMessage(message))
    }, [ ])

    React.useEffect(() => increment(), [ message ])

    return <h1>Counter: {counter}</h1>
}

createRoot(document.getElementById('root')).render(<App />)

The message sending is identical in both versions and working as expected in both cases. It is just a sendMessage call from a content script.

// contentScript.js
chrome.runtime.sendMessage({action: 'INCREMENT' })

This solution is acceptable, but I'm having trouble understanding why the second works as expected when the first does not.

Is anyone able to explain this behaviour?

Urza
  • 219
  • 1
  • 8
  • Can you show you are sending event in the first solution – Ingenious_Hans Jun 03 '22 at 12:56
  • The event sending is identical in both cases, and represents identically if logs are added to the message listener. It's just a call from sendMessage, coming from a content script `chrome.runtime.sendMessage({action: 'INCREMENT' })` – Urza Jun 03 '22 at 13:01

2 Answers2

1

It is a stale closure problem:

   function increment() {
        console.log("Incrementing ", counter, " -> ", (counter + 1));
        setCounter(counter + 1)
    }

    React.useEffect(() => {
        chrome.runtime.onMessage.addListener(message => increment())
    }, [ ])

increment will always be from first render (hence also counter), because the useEffect was run only once due to empty array as dependency.

One way to change your code will be:

React.useEffect(() => {
        // By moving increment inside useEffect it is not needed to put as dependency
        function increment() {
            // by using functional set state we always have access to recent state
            setCounter(ps => ps + 1)
        }
    
        chrome.runtime.onMessage.addListener(message => increment())
    
        return () => {        
            // You can handle remove listener here        
        }
    }, [])

But here is more reading about this topic.

Giorgi Moniava
  • 27,046
  • 9
  • 53
  • 90
0

Try to put the message listener before sending event

import React from 'react'
import { createRoot } from "react-dom/client";

function App() {
    const [ counter, setCounter ] = React.useState(0)

    function increment() {
        console.log("Incrementing ", counter, " -> ", (counter + 1));
        setCounter(counter + 1)
    }

    React.useEffect(() => {
      //first
      chrome.runtime.onMessage.addListener(message => increment())
      //then
        chrome.runtime.sendMessage({action: 'INCREMENT' })
        
    }, [ ])

    return <h1>Counter: {counter}</h1>
}

createRoot(document.getElementById('root')).render()

Ingenious_Hans
  • 724
  • 5
  • 16
  • I think there was a bit of a misunderstanding. The sendMessage call happens in a seperate file, which is a content script embedded in a 3rd party site. Receiving the message, and having it trigger the `increment()` function is working as expected in both cases, it's just that in the first the state doesn't seem to be persisted correctly and the value never changes from 1 – Urza Jun 03 '22 at 13:17