0

I came across this vue-concurrency which looks like a great library for handling asyc calls.

But I'm having trouble understanding how to implement it.

Here is a simple Vue component using vue-concurrency:

<template>
  <div>
    <div v-if="task.isRunning">Loading...</div>
    <div v-else-if="task.isError">{{ task.last?.error.message }}</div>
    <div v-else>
      {{ task.last?.value }}
    </div>
  </div>
</template>

<script setup lang="ts">
import { timeout, useTask } from 'vue-concurrency';
const task = useTask(function* () {
  yield timeout(1000);
  if (Math.random() < 0.5) {
    // lets say the API is flaky and errors out often:
    throw new Error('Ruh oh. Something went wrong.');
  } else {
    return 'tada';
  }
});
const instance = task.perform();
// Now I try to access some of the varibles
console.log(instance.isError); // runs immediatley, always returns "false"
console.log(instance.error); // runs immediatley, always returns "null"
console.log(instance.value); // runs immediatley, always returns "null"
console.log(task.isError); // runs immediatley, always returns "false"
console.log(task.last?.isError); // runs immediatley, always returns "false"
console.log(task.last?.error); // runs immediatley, always returns "null"
console.log(task.last?.value); // runs immediatley, always returns "null"
</script>

This component makes a fake API call. Pauses for one second. Then will either throw an Error or return the "tada" string.

The current code works in the template, it shows "Loading..." and then either "tada" or the error message.

But I want to access the task and instance variables inside the script tag, and at the moment they all run immediatley without waiting for the async call to execute.

I thought I could fix this by using await task.perform(). Doing so gives me the task.last?.value variable. But the template stops working (becomes blank) and the error variables don't print to the console.

So how do I correctly access the state values of a vue-concurrency Task/Instance inside the script tag?

TinyTiger
  • 1,801
  • 7
  • 47
  • 92
  • 1
    You physically cannot get the result synchronously from asynchronous call, this is explained in https://stackoverflow.com/questions/14220321/how-to-return-the-response-from-an-asynchronous-call and many others. vue-concurrency doesn't and can't solve this problem. Yes, it will be solved with `await task.perform()`. This makes the component asynchronous and requires to use https://vuejs.org/guide/built-ins/suspense.html in a parent – Estus Flask Feb 17 '22 at 07:05

1 Answers1

0

I understand this better now.

A Task creates one or more Instances, and those Instances are asynchronous. Therefore we need to handle Instances like any other async code (await, try/catch, then/catch, etc). I have included a few options for doing that.

These code examples replace everything below the line // Now I try to access some of the varibles of the opening post. The catch errors have TypeScript types and avoid EsLint errors where possible:

Option 1 - then/catch promises:

instance
  .then((response) => {
    console.log(response); // tada
    console.log(instance.value); // tada
  })
  .catch((err: Error) => {
    console.log(err.message); // 'Ruh oh. Something went wrong.'
    console.log(instance.isError); // true
    console.log(instance.error); // the Error object
    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
    console.log(instance.error.message); // 'Ruh oh. Something went wrong.'
  });

Option 2 - try/catch using async wrapper:

const wrapper = async () => {
  try {
    await instance;
    console.log(instance.value); // tada
  } catch (err) {
    if (err instanceof Error) {
      console.log(err.message); // 'Ruh oh. Something went wrong.'
    }
    console.log(instance.isError); // true
    console.log(instance.error); // the Error object
    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
    console.log(instance.error.message); // 'Ruh oh. Something went wrong.'
  }
};
void wrapper();

Option 3 - IIFE:

(async () => {
  await instance;
  console.log(instance.value); // tada
})().catch((err: Error) => {
  console.log(err.message); // 'Ruh oh. Something went wrong.'
  console.log(instance.isError); // true
  console.log(instance.error); // the Error object
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  console.log(instance.error.message); // 'Ruh oh. Something went wrong.'
});

And actually the original code can be modified because we are not calling the Instance multiple times.

A streamlined version could chain the useTask() with perform() which would then return a single Instance (as opposed to the original code which returned a Task and later assigned the Instance to a variable).

That would save some lines but also require us to update the template and remove references to the task:

<template>
  <div>
    <div v-if="instance.isRunning">Loading...</div>
    <div v-else-if="instance.isError">{{ instance.error.message }}</div>
    <div v-else>
      {{ instance.value }}
    </div>
  </div>
</template>

<script setup lang="ts">
import { timeout, useTask } from 'vue-concurrency';
const instance = useTask(function* () {
  yield timeout(1000);
  if (Math.random() < 0.5) {
    // lets say the API is flaky and errors out often:
    throw new Error('Ruh oh. Something went wrong.');
  } else {
    return 'tada';
  }
}).perform();
// Can include then/catch, try/catch etc here if you want to access the instance variables
</script>
TinyTiger
  • 1,801
  • 7
  • 47
  • 92