I am working on the react native sdk for socket mobile and for some reason I am unable to reliably set state with useState
in my react native application. The state change happens in a callback that I pass to Socket Mobile’s capture library. Sample app can be found here (for some reason hyperlinks aren’t working).
https://github.com/SocketMobile/singleentry-rn/blob/main/App.js
The callback, onCaptureEvent
, listens for various capture event types. Based on these event types, we manage the state accordingly. One way we do that is when there is a DeviceArrival
event (when a scanner connects to our iPad/Android Tablet), we then open the device so we can see it’s properties, change those properties, etc.
We also try to update a list of devices that come in. So if a device "arrives", we add it to a list and then update the state like so.
setDevices(prevDevices => {
prevDevices = prevDevices || [];
prevDevices.push({
guid,
name,
handle: newDevice.clientOrDeviceHandle,
device: newDevice,
});
return [...prevDevices];
});
This works for the first time, but then it doesn’t work properly and the remove device function we have is unable to find the right device to remove (because either an old device is left behind or there is no devices in the list at all).
I have also tried doing it this way…
setDeviceList((prevDevices) => [...prevDevices, newDevice]);
This way we’re not mutating the state value directly. However, when I do it this way, it doesn’t work at all. When I try to find the device in the device list to remove it, it shows an error saying it cannot be found.
How or why am I unable to consistently update the list? I’ve looked at a number of tuts and it seems like the second way should work. I am assuming it has something to do with the fact that it is a callback function but we’re not sure of a way around it other than this way.
We have also tried using useCallback
for onCaptureEvent
as well as making it asynchronous so we can use await
in the function. Some state items do update accordingly (we have a state value status
in the example) but maybe since it’s a string it’s easier to detect/persist the state change?
Below is the portion of the onCaptureEvent
function that we think is the culprit.
const onCaptureEvent = useCallback(
(e, handle) => {
if (!e) {
return;
}
myLogger.log(`onCaptureEvent from ${handle}: `, e);
switch (e.id) {
case CaptureEventIds.DeviceArrival:
const newDevice = new CaptureRn();
const {guid, name} = e.value;
newDevice
.openDevice(guid, capture)
.then(result => {
myLogger.log('opening a device returns: ', result);
setStatus(`result of opening ${e.value.name} : ${result}`);
setDevices(prevDevices => {
prevDevices = prevDevices || [];
prevDevices.push({
guid,
name,
handle: newDevice.clientOrDeviceHandle,
device: newDevice,
});
return [...prevDevices];
});
})
.catch(err => {
myLogger.log(err);
setStatus(`error opening a device: ${err}`);
});
break;
...
}
I am using "react": "17.0.2"
and "react-native": "0.68.2"
UPDATE 07/11/23
I discovered that when I console.log
the devices
just before the return
statement of the component’s UI, the device list is reflected correctly. I tested this with a useEffect
that gets triggered whenever devices
gets updated. See below.
useEffect(() => {
console.log('USE EFFECT: ', devices);
}, [devices]);
This ALSO logs the updated device list. It seems the only place where I am unable to keep track of the updated list is in the onCaptureEvent
function. I’m guessing this is because onCaptureEvent is a callback that is not directly invoked by the RN component. Any ideas around this? I’ve used helper functions within the component outside of the onCaptureEvent
method but it still doesn’t seem to work–or at least onCaptureEvent
is unable to find the updated state value.
Is there a way to ensure
2
Answers
I think I figured out was wrong.
I am planning on writing a blog post about it to be more in depth but basically the state WAS updating correctly and within the context of the
App
component was up to date and accessible.The problem I think lies in that fact
onCaptureEvent
is NOT called or referenced directly by anything in theApp
component. Rather, it is invoked as a callback, almost as a side effect, of a third party library (in this case the CaptureSDK).Since it is passed to the CaptureSDK and invoked because of events that occur in the SDK, more complex data structures (Arrays and Objects) are harder to persist consistently.
status
was accurately reflected because it was a string. As were other state values that were integers, booleans, etc. These are much easier data types to detect differences in.An array or object, however, is a bit more difficult for functional components to immediately detect/register. For instance, if you updated an array, set the state using the new array, then immediately tried to log
myArray
, you might not see the reflected values.So not only were we trying to access state in a callback that has a different invocation context but we are also SETTING the state within this different context. I think this combination allowed for the appearance of state not being updated or accessible.
The Solution
To remedy this, I found this question on SO where the accepted answer made use of the
useRef
hook. For example, I could useuseState
to initialize and later in the code set the value fordevices
. I could also (after initializing withuseState
) create a reference instance calledstateRef
where I can store thedevices
reference.Then in
onCaptureEvent
I can set the state as usual, but when I want to reference the latestdevices
, I can usestateRef.current
. I can use this list to find theremoveDevice
, remove it from the list and then set the state-and properly determine which device was removed.Can try this code?