英文:
State array not updating properly in React Native with useState
问题
我正在开发 Socket Mobile 的 React Native SDK,并且出现了一个问题,我无法可靠地使用 useState
来设置状态。状态的更改发生在我传递给 Socket Mobile 捕获库的回调函数中。示例应用程序可以在此处找到(由于某种原因超链接不起作用):
https://github.com/SocketMobile/singleentry-rn/blob/main/App.js
回调函数 onCaptureEvent
用于监听各种捕获事件类型。根据这些事件类型,我们相应地管理状态。其中一种方式是当出现 DeviceArrival
事件(即扫描仪连接到我们的 iPad/Android 平板电脑时),我们打开设备以查看其属性、更改这些属性等。
我们还尝试更新设备列表。所以如果一个设备“到达”,我们将其添加到列表中,然后像这样更新状态:
setDevices(prevDevices => {
prevDevices = prevDevices || [];
prevDevices.push({
guid,
name,
handle: newDevice.clientOrDeviceHandle,
device: newDevice,
});
return [...prevDevices];
});
这在第一次运行时有效,但之后它就无法正常工作了,我们的移除设备函数无法找到正确的设备以移除(因为要么旧设备被留下,要么列表中根本没有设备)。
我还尝试过这种方式...
setDeviceList(prevDevices => [...prevDevices, newDevice]);
这种方式我们不直接改变状态值。然而,当我这样做时,它根本不起作用。当我尝试在设备列表中查找设备以移除它时,会显示一个错误,说明找不到它。
为什么我无法始终更新列表?我已经查看了许多教程,似乎第二种方式应该起作用。我认为这可能与它是回调函数有关,但除了这种方式之外,我们不确定是否有其他方法。
我们还尝试在 onCaptureEvent
中使用 useCallback
,并将其设置为异步,以便在函数中使用 await
。某些状态项确实会相应更新(例如示例中的 status
状态),但可能因为它是字符串,所以更容易检测/保持状态变化?
以下是我们认为是问题所在的 onCaptureEvent
函数部分:
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;
...
}
我使用的版本是 react: "17.0.2"
和 react-native: "0.68.2"
。
更新 2023 年 07 月 11 日
我发现,当我在组件的 UI 返回语句之前使用 console.log
打印 devices
时,设备列表会正确反映。我通过使用一个 useEffect
来测试了这一点,该 useEffect
在 devices
更新时触发。请参见下面的代码:
useEffect(() => {
console.log('USE EFFECT: ', devices);
}, [devices]);
这也记录了更新后的设备列表。似乎唯一无法跟踪更新列表的地方是在 onCaptureEvent
函数中。我猜这是因为 onCaptureEvent
是一个不直接由 RN 组件调用的回调函数。对此有何想法?我已经在组件外部的 onCaptureEvent
方法中使用了辅助函数,但似乎仍然不起作用,或者至少 onCaptureEvent
无法找到更新后的状态值。
是否有办法确保...
英文:
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
答案1
得分: 1
可以尝试这段代码吗?
setDevices((device ?? []).concat({
guid,
name,
handle: newDevice.clientOrDeviceHandle,
device: newDevice,
}));
英文:
Can try this code?
setDevices((device ?? []).concat({
guid,
name,
handle: newDevice.clientOrDeviceHandle,
device: newDevice,
}));
答案2
得分: 0
我觉得我弄清楚了问题出在哪里。
我计划写一篇关于这个问题更深入的博客文章,但基本上状态是正确更新的,并且在App
组件的上下文中是最新的并且可访问的。
我认为问题在于onCaptureEvent
事实上没有被App
组件中的任何东西直接调用或引用。相反,它作为回调被调用,几乎像第三方库(在本例中是CaptureSDK)的副作用一样。
由于它被传递给CaptureSDK并且是由SDK中发生的事件触发的,更复杂的数据结构(数组和对象)更难以一致地保持。
status
被准确反映出来,因为它是一个字符串。其他状态值,如整数、布尔值等,也是如此。这些数据类型更容易检测差异。
然而,数组或对象对于函数组件来说稍微难以立即检测或注册。例如,如果您更新了一个数组,使用新数组设置状态,然后立即尝试记录myArray
,您可能看不到反映出的值。
因此,我们不仅试图在具有不同调用上下文的回调中访问状态,而且还在这个不同上下文中设置状态。我认为这种组合导致了状态似乎没有更新或不可访问的外观。
解决方案
为了解决这个问题,我在Stack Overflow上找到了这个问题,接受的答案使用了useRef
钩子。例如,我可以使用useState
来初始化devices
的值,并且稍后在代码中设置该值。我还可以(在使用useState
初始化后)创建一个名为stateRef
的引用实例,用于存储devices
的引用。
const App = () => {
const [devices, setDevices] = useState([]);
const stateRef = useRef();
stateRef.current = devices;
// ...
}
然后在onCaptureEvent
中,我可以像往常一样设置状态,但当我想引用最新的devices
时,我可以使用stateRef.current
。我可以使用这个列表来查找removeDevice
,从列表中删除它,然后设置状态,并正确确定哪个设备已被删除。
case CaptureEventIds.DeviceRemoval:
let devs = [...stateRef.current];
let index = devs.findIndex((d) => {
return d.guid === e.value.guid;
});
if (index < 0) {
myLogger.error(`no matching devices found for ${e.value.name}`);
return;
}
let removeDevice = devs[index];
devs.splice(index, 1);
setDevices(devs);
break;
英文:
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 the App
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 use useState
to initialize and later in the code set the value for devices
. I could also (after initializing with useState
) create a reference instance called stateRef
where I can store the devices
reference.
const App = () =>{
const [devices, setDevices] = useState([])
const stateRef = useRef();
stateRef.current = devices
...
}
Then in onCaptureEvent
I can set the state as usual, but when I want to reference the latest devices
, I can use stateRef.current
. I can use this list to find the removeDevice
, remove it from the list and then set the state-and properly determine which device was removed.
case CaptureEventIds.DeviceRemoval:
let devs = [...stateRef.current];
let index = devs.findIndex((d) => {
return d.guid === e.value.guid;
});
if (index < 0) {
myLogger.error(`no matching devices found for ${e.value.name}`);
return;
}
let removeDevice = devs[index];
devs = devs.splice(index, 1);
setDevices(devs);
break;
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论