State array not updating properly in React Native with useState

huangapple go评论59阅读模式
英文:

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 来测试了这一点,该 useEffectdevices 更新时触发。请参见下面的代码:

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 = () =&gt;{
  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) =&gt; {
        return d.guid === e.value.guid;
   });

   if (index &lt; 0) {
      myLogger.error(`no matching devices found for ${e.value.name}`);
       return;
   }

   let removeDevice = devs[index];
   devs = devs.splice(index, 1);
   setDevices(devs);
   break;

huangapple
  • 本文由 发表于 2023年7月11日 05:05:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/76657336.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定