CRITICAL: Writes/Updates to Canister from React Native (Expo) Lock Up UI and Make App Unresponsive

Hi Everyone - I got DSocial working on React Native with Expo. Wooohooo. It’s an awesome dev experience. Lots of hacks to get it working, but it now works.

Critical issue I have is on physical iOS devices (currently testing on iPhone 11 Pro Max), not virtual devices e.g. Simulators. If at any point I make a call to function on the canister that is NOT a query i.e. a write/update func, in this example an increment function…

The entire app becomes unresponsive. It’s taken me around 3 weeks to find this issue through the sea of code I’ve written, it’s actually very simple, I can make query calls all day long, performance of app is great. BUT If at any time I make a call to function that is a write/update func then the app becomes laggy and unresponsive.

Really need the help of DFINITY team and any React Native devs out there, to help find what could be causing the issue. My thoughts is it’s something do with the BLS verification? I don’t know. But currently DSocial is blocked on releasing to app store until this is resolved. Please look at the patches folder, maybe one of these is causing the issue.

I’ve packed the code into a simple increment/get example repo, here: GitHub - ashr1987/expo-rn-dfinity-icp-slow-bug: Example React Native (in Expo) that shows the UI performance lock up on iOS Devices after making write calls to a canister

I’ve deployed the canister to the mainnet, so you can run this code on a physical iOS device to see the issue live. Instructions are in the README. Hope someone can help :pray:

6 Likes

Im not sure myself I can help, but I will escalate this internally to see if anyone can.

1 Like

Looks like you’re making a call from a rendering function (since you’re using functional components).

Try to move inc() function declaration outside the rendering function.

It’s called when you click a button, not when the component is rendered, moving the function declaration outside function makes no difference. I’ve tried many variations. If the function is never called, performance stays normal, if at any point it is called the UI locks up. This is only on physical devices, not simulators so clearly an issue inside JS UI thread on device.

1 Like

any update @diegop? :slight_smile:

@Ashley I saw your tweet and pinged a few others from the team as well, waiting to hear back.

3 Likes

It depends on your set up. AFAIK iOS 15 devices doesn’t enable WebAssembly, which is available in the simulator. Have you tried on Android?

Thanks jzxchiang

How do you get around this issue on RN on iOS devices then? Do I need to change the WebKit version? The device I was testing on was on iOS 14, now upgrading to iOS v15 to see if it makes a difference.

Android is get the same problem, but when making query calls too.

Do you have an app on RN talking to IC without these issues? If so, do you have an example I can see?

This a kinda a deal breaker for making DSocial work on IC :frowning:

Update: Alexa and I pinged people within DFN, so far no one has felt comfortable with their react native experience, but well keep looking.

Yeah, it would be me, most likely, and you’re pushing past what I’ve been able to develop a proof of concept for while I’m working on other features. @jzxchiang is the only person I know of who has successfully gotten around the webassembly issue, and we certainly don’t have a smooth path for you yet, unfortunately

1 Like

If you were testing on an iOS 14 simulator and it worked but then tested on an iOS 14 device and it didn’t work, then it’s likely not due to the WebAssembly issue. Especially if Android has the same problem.

I wrote up my findings on how to get a RN mobile app talk to IC here. What you’re running into should be one of the issues I described.

3 Likes

I had a quick try to the sample repo even though I don’t have much experience with React Native.
I can also reproduce the error on my real iPhone.

However, interesting things to note, the update calls themselves seem to succeed. OP example is a simple counter and I do notice the counter going up if I call the increment. On the other hand, the promise that call the backend does not resolve nor throws any errors.

from @Ashley provided code sample:

const inc = async () => {
    const start = Date.now()
    const backend = await getBackendActor()

    console.log('so far so good')
   
    await backend.inc() // backend is actually incremented

    console.log(`we do not reach this point - promise does not resolve`)
  } catch (err) {
     // no errors are thrown
  }
1 Like

What happens if you don’t await backend.inc()?

Meaning:

- await backend.inc()
+ backend.inc()

Thanks @paulyoung @peterparker @jzxchiang - really appreciate you looking at this. Weeks of pain :slight_smile:

I’ve tried all variations of this, basically that promise does actually get resolved, it just takes about 30-60 seconds. If this inc() is called in anyway (with or without await), it blocks the UI thread and the app is unresponsive.

Even once the promise finally resolves the app is still slow and unresponsive. On simulator, zero issues.

I’m now porting the code to standard RN and not with Expo, to see if it’s something to do with Expo. Pain.

1 Like

Does the agent use a for loop anywhere? Loops don’t play nice with browsers. I’ve run into this with agent-rs → agent-rs-wasm and I had to move from a loop { read_state, if state is answered break …} to proper web-based intervals. I would also take a look at the agent to see if it has any kind of call_raw functions, so you can test just sending the update (and not checking for a response) to narrow down where the problem could be.

To follow @GLDev hypothesis I added some hardcoded console.log in the node_modules of agent-js (I’m not familiar with) of OP’s sample repo to check if pollForResponse would be called often.

Doing so, I actually noticed that the app is using the CJS bundle of agent-js and not the esm one. I am not familiar with react native enough, is that correct???

In frontend CJS is use on the backend side, in the browser we use the esm modules - CJS does not work in the browser. In addition, when I use CJS, I explicitly have to specify which fetch library I want to use with agent-js (passing fetch to agent-js).

So it is correct? And if yes, should also a fetch library be configured by the app?

1 Like

Its not a best approach, but you can use InteractionManager from React Native to show loading indicator while JavaScript is busy.
# InteractionManager

The biggest bottle neck of React Native is that, it is single Javascript threaded process.
Here are alternative solutions - runAfterInteractions, react-native-threads or react-native-webview

I don’t have an iPhone available to test if this will work, but here’s one workaround you can see if this fixes the problem.

Change the React Navigation navigation.navigate(…) call to use navigation.push(…) call, since what I imagine might be happening is that the screen that originally does the write call is still on the App’s screen stack, so even though you’ve backed out of the screen, when you try to navigate back to it, it’s the same screen still awaiting the write call previously made and as a result locks up the screen.

By default React Navigation returns any instances of a screen already created and on the App’s screen stack before creating any new instances of a specific screen. However, it does provide a way to create new instances of a given screen, by instead using push(…) instead of navigate(…) on its navigation object.

See here: https://reactnavigation.org/docs/navigating/#navigate-to-a-route-multiple-times

While this may not be the desirable outcome since you’ll be creating new screens, if would at least eliminate that as the culprit if it doesn’t solve the problem.

Alternatively, you could make a custom hook (similar to the one you have for loading fonts, but for handling all logic relating to canisters) that injects a wrapped context provider using useeffect to initialize the provider’s value by getting the canister’s data and caching it into an async storage (or the similar encrypted library Expo recommends in case it is sensitive data), which it could provide to any screen(s) wishing to access that data. If the custom hook knows an update is pending due to user action, it could poll the canister for the new state (as others have suggested) and when available update itself and rerender consumers otherwise provide the cached value.

This custom hook could be combined with (or you could use this by itself) BackgroundFetch - Expo Documentation and/or How to run background tasks in Expo React Native | Chafik Gharbi to handle all interaction with the canister, so that the app itself only communicates to the canister through this background task through this custom hook.

Another thing to note is instead of using useState, you could use useReducer and dispatch when you either need to get or set state which would act as a processing queue and thus help avoid blocking. The --really-- cool thing about useReducer is it’s closer to a state machine and so closer to being deterministic which is an ideal feature of any dapp.

Hope this helps!

*It also occurred to me that you might need to use useRef to hang on to the actor reference in your component, because if the component is continually recreating that reference (as it would with every rerender unless, for instance, it’s a useRef reference) this would also explain why it becomes laggy/unresponsive since each time the component is rerendered a new reference to the actor is created in addition to repeating the query call from the useEffect.

2 Likes

Thanks everyone - I tried everything mentioned here, it still locks the app up. Either with raw RN, Expo, tiny sample app or DSocial full app.

Honestly after almost 4 weeks of grinding on this problem, I’m so desperate, that I’m now building a webview to make the calls to IC, it works but god knows how I’m going to be able to support authenticated/logged in users.

Does anyone have an RN app in the App Store that connects directly to IC?

I found the issue :raised_hands:

It’s the pollForResponse function. If this is not called the app stays performant without any issues.

Here’s the link in the source code: agent-js/actor.ts at f07ed959b5697f27ed360d94a140b859bbc054e0 · dfinity/agent-js · GitHub

Obviously this is not a fix, but for now the functions I’m calling I don’t care about the response, e.g. incrementView on a video etc etc.

UNBLOCKED after 4 weeks of pain, thanks for everyones support above :two_hearts: :two_hearts: :two_hearts:

16 Likes