react-native-reanimated icon indicating copy to clipboard operation
react-native-reanimated copied to clipboard

Make worklets, runOnUI and runOnJS awaitable

Open mrousavy opened this issue 3 years ago • 4 comments

Description

Makes worklets (runOnUI) and runOnJS return a Promise so it can be awaited and can now return values.

const result = await runOnUI(() => {
  return 'Test!'
})();
console.log(result);

Changes

  • Makes worklets (runOnUI) return a Promise
  • Makes hostFunction (runOnJS) return a Promise

Test code and steps to reproduce

Test await:

  const run = async () => {
    console.log('in run')
    const x = await runOnUI(() => {
      'worklet';
      return 'result from worklet'
    })()
    console.log(`runOnUI result: ${x}`);
  };
  run();

Test errors thrown being Promise.rejected:

      try {
        const result = await runOnUI(() => {
          'worklet';
          throw new Error("Failed!")
        })();
      } catch (e) {
        console.error(`Error: ${e.message}`)
      }

Test awaiting runOnJS in a worklet:

const run = async () => {
  console.log('in run')
  const x = await runOnUI(async () => {
    'worklet';
    console.log('in runOnUI')
    const res = await runOnJS(doSomethingOnJS)();
    console.log(`runOnJS result: ${res}`)
    return 'yooo'
  })()
  console.log(`runOnUI result: ${x}`);
};
run();

⚠️ warning: the above snippet still crashes because the babel plugin requires additional transformation steps so it can successfully transform async/await code. Right now it just submits the above code with the await statement as is, and that crashes the WorkletsCache eval code because you cannot use top-level await there. I'm waiting on @piaskowyk / @Szymon20000 's feedback here.

Checklist

  • [x] Included code example that can be used to test this change
  • [x] Updated TS types
  • [ ] Added TS types tests
  • [ ] Added unit / integration tests
  • [ ] Updated documentation
  • [x] Ensured that CI passes

Missing

When calling from JS -> UI it successfully returns a Promise which now can be awaited. When calling from UI -> UI, it does not return a Promise, but rather returns the return value of the worklet as is: https://github.com/software-mansion/react-native-reanimated/blob/afa2791ed3c95de7c0c2d869104994a5362f0928/Common/cpp/SharedItems/ShareableValue.cpp#L354

I tried to use Promise.resolve(res) here, but this always gave me crashes because apparently some code depends on this behaviour, so I just left it as it is right now. This will likely never affect the user, as he never calls runOnUI(() => runOnUI...)().

mrousavy avatar Mar 31 '21 09:03 mrousavy

I've also managed to make runOnJS awaitable, but you cannot create worklets with async functions. So the following code:

const run = async () => {
  console.log('in run')
  const x = await runOnUI(async () => {
    'worklet';
    console.log('in runOnUI')
    const res = await runOnJS(doSomethingOnJS)();
    console.log(`runOnJS result: ${res}`)
    return 'yooo'
  })()
  console.log(`runOnUI result: ${x}`);
};
run();

crashes because the eval() func in WorkletsCache cannot accept an async function with an await statement, but this code:

const run = async () => {
  console.log('in run')
  const x = await runOnUI(async () => {
    'worklet';
    console.log('in runOnUI')
    runOnJS(doSomethingOnJS)().then((res) => {
      console.log(`runOnJS result: ${res}`)
    });
    return 'yooo'
  })()
  console.log(`runOnUI result: ${x}`);
};
run();

works perfectly fine. We can update the types so runOnUI does not accept async functions, and users can use .then and .catch to "await" results. This is not ideal, but it works for now. Alternatively, we can adjust the babel plugin to transform async/await code so it can be evaluated by WorkletsCache, and the user can use await inside of worklets.

What do you think @Szymon20000 @piaskowyk ?

mrousavy avatar Mar 31 '21 10:03 mrousavy

Oh maybe that async worklet kept crashing because Hermes doesn't support async/await yet? 🤔

mrousavy avatar Apr 02 '21 08:04 mrousavy

@mrousavy by using this spawnThread is not showing console.log but showing native log + console.log complete block + runOnJS cannot call api

numandev1 avatar May 11 '21 09:05 numandev1

Ah, it took me a while to realize async function cannot be put inside runOnJS.

I was calling a mutateAsync function from react-query to make a post request when swiping a card left or right in tinder like fashion.

Fortunately in this case I could just switch to a synchronous mutate function.

Either way, this PR from @mrousavy looks like it would be a nice addition.

vbylen avatar Aug 06 '22 01:08 vbylen

After the Shareables rewrite recently merged into v3, this would also need to be rewritten from scratch. It's also not obvious how the inclusion of this feature fits into Reanimated. If you're still interested in this, feel free to open a new PR with an updated implementation.

jwajgelt avatar Dec 16 '22 15:12 jwajgelt