skip to Main Content

I am building a React Native (Expo) app that scans for Bluetooth devices. The Bluetooth API exposes a callback for when devices are detected, which I use to put non-duplicate devices into an array:

const DeviceListView = () => {
  const [deviceList, setDeviceList] = useState([]);
  
  const startScanning = () => {
    manager.startDeviceScan(null, null, (error, device) => {
        // Add to device list if not already in list
        if(!deviceList.some(d => d.device.id == device.id)){
          console.log(`Adding ${device.id} to list`);
          const newDevice = {
            device: device,
            ...etc...
          };
          setDeviceList(old => [...old, newDevice]);
        }
    });
  }

  // map deviceList to components
  componentList = deviceList.map(...);

  return <View> {componentList} </View>
}

The problem is that the callback is called many many times faster than setDeviceList updates, so the duplicate checking doesn’t work (if I log deviceList, it’s just empty).

If I use an additional, separate regular (non-useState) array, the duplicate checking works, but the state doesn’t update consistently:

const DeviceListView = () => {
  const [deviceList, setDeviceList] = useState([]);
  var deviceList2 = [];
  
  const startScanning = () => {
    manager.startDeviceScan(null, null, (error, device) => {
        // Add to device list if not already in list
        if(!deviceList2.some(d => d.device.id == device.id)){
          console.log(`Adding ${device.id} to list`);
          const newDevice = {
            device: device,
            ...etc...
          };
          deviceList2.push(newDevice);
          setDeviceList(old => [...old, newDevice]);
        }
    });
  }

  // map deviceList to components
  componentList = deviceList.map(...);

  return <View> {componentList} </View>
}

This code almost works, but the deviceList state doesn’t update correctly: it shows the first couple of devices but then doesn’t update again unless some other component causes a re-render.

What do I need to do to make this work as expected?

2

Answers


  1. convert your callback to promise so that until you get completed device list, checkout below code (PS. not tested, please change as you need)

      const [deviceList, setDeviceList] = useState([]);
      const [scanning, setScanning] = useState(false);
    
      useEffect(() => {
        if(scanning) {
          setDeviceList([]);
          startScanning();
        }
      }, [scanning]);
    
      const subscription = manager.onStateChange(state => {
        if (state === "PoweredOn" && scanning === false) {
          setCanScan(true);
          subscription.remove();
        }
      }, true);
    
      const fetchScannedDevices = () => {
        return new Promise((resolve, reject) => {
          manager.startDeviceScan(null, null, (error, device) => {
            // Add to device list if not already in list
            if (!deviceList.some(d => d.device.id == device.id)) {
              console.log(`Adding ${device.id} to list`);
              const newDevice = {
                device: device,
                // ...etc...
              };
              resolve(newDevice);
            }
            if (error) {
              reject({});
            }
          });
        });
      };
    
      const startScanning = async () => {
        try {
          const newDevice = await fetchScannedDevices();
          setDeviceList(old => [...old, newDevice]);
        } catch (e) {
          //
        }
      };
    
      const handleScan = () => {
        setScanning(true);
      };
    
      // map deviceList to components
      componentList = deviceList.map(() => {});
    
      return (
        <View>
          <Button
            onPress={() => handleScan()}>
            Scan
          </Button>
          <View>{componentList}</View>
        </View>
      );
    };
    
    Login or Signup to reply.
  2. I would suggest wrap your duplicate check within the state set function itself, and then return the same device list if no new devices have been found. This offloads race condition handling to the underlying react implementation itself, which I’ve found to be good enough for most cases.

    Thus it would look something like this:

    const DeviceListView = () => {
      const [deviceList, setDeviceList] = useState([]);
      
      const startScanning = () => {
        manager.startDeviceScan(null, null, (error, device) => {
            // Add to device list if not already in list
            setDeviceList(old => {
              if(!old.some(d => d.device.id == device.id)){
                console.log(`Adding ${device.id} to list`);
                const newDevice = {
                  device: device,
                  // ...etc...
                };
                return [...old, newDevice]
              }
    
              return old
            });
        });
      }
    
      // map deviceList to components
      componentList = deviceList.map(...);
    
      return <View> {componentList} </View>
    }
    

    Since old is unchanged if no new unique devices are found it will also skip next re-render according to the docs ( which is a neat optimisation 🙂 )

    This is the preferred way to implement state updates that are dependant on previous state according to the docs
    https://reactjs.org/docs/hooks-reference.html#functional-updates

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search