0

I am new to using React and Recoil and want to display real-time charts (using D3) from data that is gathered in real-time using the Web Bluetooth API.

In a nutshell, after calling await myCharacteristic.startNotifications() and myCharacteristic.addEventListener('characteristicvaluechanged', handleNotifications), the handleNotifications callback is called each time a new value is notified from a Bluetooth device (see this example).

I am using hooks and tried to modify a recoil state from the callback (this was simplified to the extreme, I hope it is representative):

export const temperatureState = atom({
  key: 'temperature',
  default: 0
})

export function BluetoothControls() {
  const setTemperature = useSetRecoilState(temperatureState);

  const notify = async () => {
    ...
    temperatureCharacteristic.addEventListener('characteristicvaluechanged', event => {
      setTemperature(event.target.value.getInt16(0))
    }
  }
  return <button onClick={nofity}/>Start notifications</button>
}

This work fine if I want to display the latest value somewhere in the app. However, I am interested in keeping the last few (let's say 10) values in a circular buffer to draw a D3 chart.

I tried something along the lines of:

export const temperatureListState = atom({
  key: 'temperature-list',
  default: []
})

export function BluetoothControls() {
  const [temperatureList, setTemperatureList] = useRecoilState(temperatureListState);

  const notify = async () => {
    ...
    temperatureCharacteristic.addEventListener('characteristicvaluechanged', event => {
      let temperatureListCopy = temperatureList.map(x => x);
      temperatureListCopy.push(event.target.value.getInt16(0))
      if (temperatureListCopy.length > 10)
        temperatureListCopy.shift()
      setTemperatureList(temperatureListCopy)
    }
  }
  return <button onClick={nofity}/>Start notifications</button>
}

However, is is pretty clear that I am running into the issue described here where the function is using an old version of temperatureList that is captured during render. As a result, temperatureState is always empty and then replaced with a list of one element.

How to maintain a consistent list in a React state/Recoil atom that is updated from an external callback? I think this issue is a bit similar but I'd like to avoid using another extension like Recoil Nexus.

DurandA
  • 1,095
  • 1
  • 17
  • 35

1 Answers1

0

useSetRecoilState accepts an updater function as an argument with the value to be updated as the first parameter:

export function BluetoothControls() {
  const setTemperatureList = useSetRecoilState(temperatureListState);

  const notify = async () => {
    ...
    temperatureCharacteristic.addEventListener('characteristicvaluechanged', event => {
      setTemperatureList(t => {
        let temperatureListCopy = t.map(x => x);
        temperatureListCopy.push(event.target.value.getInt16(0))
        if (temperatureListCopy.length > 10)
          temperatureListCopy.shift()
        return temperatureListCopy
      })
    }
  }
  return <button onClick={nofity}/>Start notifications</button>
}

This solves the issue as the updater function is only evaluated on events.

DurandA
  • 1,095
  • 1
  • 17
  • 35