Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v-await / v-then / v-catch directives #199

Open
CapitaineToinon opened this issue Aug 9, 2020 · 9 comments
Open

v-await / v-then / v-catch directives #199

CapitaineToinon opened this issue Aug 9, 2020 · 9 comments

Comments

@CapitaineToinon
Copy link

What problem does this feature solve?

I've been using svelte for a while and I completely fell in love with await block. In svelte, if you have a function that returns a promise, it can be very easily awaited directly in the template and it's really amazing to use.

<script>
	let promise = getUser();

	async function getUser() {
		const resp = await fetch('https://www.example.com/api/users/1')
		return resp.json();
	}
</script>

{#await promise}
	<p>...waiting</p>
{:then number}
	<p>The number is {number}</p>
{:catch error}
	<p style="color: red">{error.message}</p>
{/await}

And I think something similar would be awesome in Vue 3. I've always wanted a feature like this in Vue 2 and while there are solutions like handing everything yourself manually or using a plugin like vue-wait, it never felt like a nice solution. Look at an example on how you could currently handle such a case.

<template>
  <div v-if="error">
    {{ error.message }}
  </div>
  <div v-else-if="user === undefined">
    Loading...
  </div>
  <div v-else>
    Hello, {{ user.name }}!
  </div>
</template>

<script>
export default {
  data: () => ({
    user: undefined,
    error: null,
  }),
  methods: {
    async getUser() {
      this.user = undefined;
      this.error = null;

      try {
        const resp = await fetch('https://www.example.com/api/users/1')
        this.user = resp.json();
      } catch (error) {
        this.error = error;
      }
    }
  },
  mounted() {
    this.getUser();
  }
};
</script>

What does the proposed API look like?

Using await, then and catch

<template>
  <div v-await="getUser()">
    Loading user...
  </div>
  <div v-then="{ user }">
    Hello, {{ user.name }}!
  </div>
  <div v-catch="{ error }">
    {{ error }}
  </div>
</template>

Only using await and then on a single element

<template>
  <div v-await="getUser()" v-then="{ user }">
    Hello, {{ user.name }}!
  </div>
  <div v-catch="{ error }">
    {{ error }}
  </div>
</template>

The catch could be optional

<template>
  <div v-await="getUser()">
    Loading user...
  </div>
  <div v-then="{ user }">
    Hello, {{ user.name }}!
  </div>
</template>
@Mdev303
Copy link

Mdev303 commented Aug 10, 2020

What if we have multiple promises to v-await and we want to show them with v-then but with a custom order ?

@posva
Copy link
Member

posva commented Aug 10, 2020

I don't think this is particularly useful because it only works in scenarios where you want to either display a loading placeholder or the content itself. But in practice, you still want to display parts of the UI while loading, replacing only portions of the UI with placeholders or spinners.
With this approach there is no granularity. It works well on small demos but in real use cases, you rarely do it that way. Specially given the trend of placeholders that exists everywhere right now (https://github.com/michalsnik/vue-content-placeholders) that benefit from reusing an existing layout

At the end, this is just another syntax that can be worked out in plugins like https://github.com/posva/vue-promised or https://github.com/posva/vue-compose-promise

@CapitaineToinon
Copy link
Author

CapitaineToinon commented Aug 10, 2020

@maazdev What if we have multiple promises to v-await and we want to show them with v-then but with a custom order ?

The content if each v-await, v-then and v-catch would be conditionally rendered like a v-if so the content of a v-then would only be processed once the promise is resolved which would allow you to nest other v-await in the v-then if desired

<template>
    <div v-await="getUser()">
        Loading user...
    </div>
    <div v-then="{ user }">
        <div v-await="getBlogs(user.id)">
            Loading blogs...
        </div>
        <div v-then="{ blogs }">
            <ul>
                <li v-for="blog in blogs" :key="blog.url">
                    <a :href="blog.url">{{ blog.title }}</a>
                </li>
            </ul>
        </div>
        <div v-catch="{ error }">
            {{ error }}
        </div>
    </div>
    <div v-catch="{ error }">
        {{ error }}
    </div>
</template>

If there are too many nesting required, nothing prevents you from doing a bigger promise that returns all the data you need and only wait for that one.

<template>
    <div v-await="getData()">
        Loading user and blogs...
    </div>
    <div v-then="{ data }">
        <h1>{{ data.user.name }}</h1>
        <ul>
            <li v-for="blog in data.blogs" :key="blog.url">
                <a :href="blog.url">{{ blog.title }}</a>
            </li>
        </ul>
    </div>
    <div v-catch="{ error }">
        {{ error }}
    </div>
</template>

<script>
export default {
    methods: {
        async getData() {
            const user = await getUser();
            const blogs = await getBlogs(user.id);

            return {
                user,
                blogs
            }
        }
    }
}
</script>

With this example you would need to access data.user and data.blogs though which feels pretty ugly. In Svelte you can destructure directly in the then block but I don't know if a syntax like this would be possible with Vue. Thoughts?

<script>
function getData() {
        // logic here
        return [user, blogs]
}
</script>

{#await getData()}
	<!-- promise is pending -->
	<p>waiting for the promise to resolve...</p>
{:then [user, blogs]}
	<!-- promise was fulfilled, can now use user and blogs variables -->
{/await}

@posva I don't think this is particularly useful because it only works in scenarios where you want to either display a loading placeholder or the content itself. But in practice, you still want to display parts of the UI while loading, replacing only portions of the UI with placeholders or spinners.

Of course and since this proposal would only add directives, you could use those on selected elements of the UI only. You're later talking about placehoders and this could work very well compined with v-await. Consider this example where we already have the user object but need to fetch the user's profile picture, we could display all the currently available data while using the await directive to show a placeholder for the image in the meantime.

<template>
    <div class="user-card">
        <p>{{ user.name }}</p>

        <ul>
            <!-- displaying some already fetched data -->
            <li v-for="social in user.links" :key="social.id">
                <a :href="social.url">{{ social.name }}</a>
            </li>
        </ul>

        <!-- but displaying a loading placeholder for other parts of the UI -->
        <div v-await="getUserImage(user.id)">
            <content-placeholders-heading :img="true" />
        </div>
        <div v-then="{ img }">
            <img :src="img" :alt="`${user.name}'s profile picture`">
        </div>
    </div>
</template>

With this approach there is no granularity.

Maybe I wasn't clear enough in my first example but I picture this working very similarly to how v-if / v-else-if / v-else currently work. So for example you would need to the using a v-then alone could trigger and error such as 'v-then' directives require being preceded by the element which has a 'v-await' directive. and so on.

At the end, this is just another syntax that can be worked out in plugins [...]

Yes I agree that there are already solutions available but I thought this proposal would be a nice syntaxic sugar that would benefit Vue as a whole.

@posva
Copy link
Member

posva commented Aug 10, 2020

Consider this example where we already have the user object but need to fetch the user's profile picture, we could display all the currently available data while using the await directive to show a placeholder for the image in the meantime.

It's not what I'm talking about. I'm talking about fetching the whole content and still displaying the main layout of the app while waiting for that content like airbnb, Vercel, sometimes Github, Facebook, instagram. If the v-await is at the top nothing is displayed until you get the data. or you duplicate the layout:

 <div v-await="getUser(user.id)">
layout with placeholders
        </div>
        <div v-then="{ img }">
same layout with the actual information
        </div>

@CapitaineToinon
Copy link
Author

CapitaineToinon commented Aug 10, 2020

If the v-await is at the top nothing is displayed until you get the data. or you duplicate the layout

Yes but that would be the developer's fault, not a design flaw of the feature. Consider this example using v-if, my app is basically empty until the user had loaded. That sucks I agree but hey that's how I wrote my app so shame on me.

<template>
    <div v-if="user">
        <h1>{{ user.name }}</h1>
    </div>
    <!-- app is unavailable while data is loading but hey that's my fault -->
</template>

<script>
export default {
    data: () = ({
        user: undefined
    })
    methods: {
        async getUser() {
            // await logic that is very slow
            return user;
        }
    },
    async mounted() {
        this.user = await this.getUser()
    }
}
</script>

So yes v-await could be use badly but that's true for everything. Edit: unless I still don't understand what specific case you're talking and if so I apologize.

@nekosaur
Copy link

Your examples are too small so they will never highlight the problem. I think what @posva is trying to get at is that if the data you are fetching is meant to be displayed in different or multiple parts of the UI, you will be forced to hoist the v-await directive to the highest common element. Which could lead to template duplication, and large parts of the UI not being rendered until the promise resolves.

Here's part of an imaginary profile page of a user.

<div>
  <skeleton type="avatar" :active="!user">
    <avatar :src="user.avatar" />
  </skeleton>
  <grid>
    <row>
      <skeleton type="paragraph" :active="!user">
        <p>{{user.about}}</p>
      </skeleton>
    </row>
	...
  </grid>
  ...
</div>

With v-await you'd have to put the directive on the top div, or you'd be making multiple identical calls to the backend (and it would be way more verbose). Thus you'd have to duplicate the layout.

<div v-await="getUser()">
  <skeleton type="avatar" />
  <grid>
    <row>
      <skeleton type="paragraph"/>
    </row>
    ...
  </grid>
  ...
</div>
<div v-then>
  <avatar :src="user.avatar"/>
  <grid>
    <row>
      <p>{{user.about}}</p>
	</row>
    ...
  </grid>
  ...
</div>
<div v-catch>
  error message, or maybe you'd still want the layout which means another duplication
</div>

@CapitaineToinon
Copy link
Author

CapitaineToinon commented Aug 13, 2020

Thanks for explaining, now I understand the problem and @posva being the author of vue-promised, I assume that's what his combined slot is meant to solve.

I still think the v-await / v-then / v-catch could remove a lot of boilerplate for more simple cases though where skeletons aren't needed. I personally never use skeletons because I don't have apps on a scale that requires it but I also don't really know how popular that practice is.

Maybe another v-promise directive behaving like the combined slot from vue-promised could be a solution but in that case I think it's getting too specific and the user should just handle the case manually or use a 3rd party library.

@posva
Copy link
Member

posva commented Aug 13, 2020

Exactly, using a 3rd party library yields the same result and expressiveness than a native feature that would only be used in simple cases. That's why I don't think this is needed

@coolCucumber-cat
Copy link

@CapitaineToinon I don't understand why you think the same thing can't be achieved with v-if and v-else. In that example you can you can just use a v-else for when it's loading. Even better, you could have an explicit loading variable. At the start of the function set the data and error to null and loading to true, once it's finished loading you can set loading to false and set the data and error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants