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

Nuxt 3: Introducing fetch() hook #27

Open
Atinux opened this issue Mar 14, 2019 · 13 comments

Comments

@Atinux
Copy link
Member

commented Mar 14, 2019

  • Start Date: 2019-02-26
  • Target Major Version: 3.0
  • Reference Issues: #3776, #32 and #127
  • Implementation PR: nuxt/nuxt.js#5118

Summary

Vue 2.6 introduced the serverPrefetch hook on SSR. Allowing to have an asynchronous hook for components to be awaited before rendering the HTML.

The idea is to introduce a new hook called fetch() that will allow any component to handle asynchronous operation on both server-side and client-side.

Basic example

This is how a page component can look like:

pages/index.vue

<template>
  <div>
    <h1>Blog posts</h1>
    <p v-if="$isFetching">Fetching posts...</p>
    <ul v-else>
      <li v-for="post of posts" :key="post.id">
        <n-link :to="`/posts/${post.id}`">{{ post.title }}</n-link>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data () {
    return {
      posts: []
    }
  },
  async fetch () {
    this.posts = await fetch('https://jsonplaceholder.typicode.com/posts').then((res) => res.json())
  }
}
</script>

You can see a more detailed example and documentation here: https://github.com/nuxt/nuxt.js/tree/feat/async-data/examples/v3/fetch

Motivation

The main motivation here is to remove the correlation between pages & asynchronous data. Each component could have its own async data logic.

This could also introduce a way for Nuxt modules author to create components to fetch data on particular endpoints.

Example:

<template>
  <Post :post-id="$route.params.id" v-slot="{ post }">
    <h1>{{ post.title }}</h1>
    <Author :user-id="post.userId" />
    <pre>{{ post.body }}</pre>
  </Post>
</template>

<script>
import Post from '~/components/Post.vue'
import Author from '~/components/Author.vue'

export default {
  components: {
    Post,
    Author
  }
}
</script>

Where ~/components/Post.vue is something like:

<template>
  <div v-if="$isFetching">Fetching post #{{ postId }}...</div>
  <div v-else>
    <slot v-bind:post="post">
      <h1>{{ post.title }}</h1>
      <pre>{{ post.body }}</pre>
    </slot>
  </div>
</template>

<script>
export default {
  props: {
    postId: {
      type: Number,
      required: true
    }
  },
  data () {
    return {
      post: {}
    }
  },
  async fetch() {
    this.post = await fetch(`https://jsonplaceholder.typicode.com/posts/${this.postId}`).then((res) => res.json())
  }
}
</script>

Detailed design

A schema is worth a thousand words 😄

Vue js_ fetch() hook by Nuxt js (3)

Drawbacks

Context

fetch hook does not receive any context as 1st argument anymore since it has access to this.

The context will be updated to be available through this.$ctx, this.$config and this.$nuxt, learn more on #25

Server-side

No drawbacks since Nuxt will wait for all fetch hooks to be finished before rendering the page.

Client-side

The main drawback of this current implementation if the UX between Nuxt 2 & Nuxt 3 when navigating from page to page.

Let's take an example of having two pages (A and B) with fetch used in both of these pages.

  • Current behaviour: A -> middleware -> fetch/asyncData -> B
  • Nuxt 3 behaviour: A -> middleware -> B -> fetch

This implies to create placeholders to display something while fetch is being called. This is why $isFetching is introduced.

We could support the "old" behaviour by providing a Nuxt module (using Nuxt middleware + plugin), a POC has been made on https://github.com/nuxt/nuxt.js/tree/feat/async-data/examples/v3/async-data

Advantages

People coming from Vue applications should find the new usage of fetch easier.

  • They have access to this, no need to learn some mystery Nuxt context
  • They actually use beforeMount or mounted hook to call asynchronous data, renaming it to fetch should just work
  • Pages transitions will be easier since it will quickly switch to new page since fetch is called during beforeMount hook

Unresolved questions

  • Should we introduce a placeholder property to overwrite render and show this component until $isFetching becomes false?
  • Do we need to support default placeholder for faster development experience? (something like https://github.com/LucasLeandro1204/vue-content-loading)
  • Should we display the error page if a component has an unexpected error in the fetch hook?

Things left:

@Atinux Atinux self-assigned this Mar 14, 2019

@Atinux Atinux added the nuxt 3 label Mar 14, 2019

@Atinux Atinux referenced this issue Mar 14, 2019
1 of 6 tasks complete
@Gregg

This comment has been minimized.

Copy link

commented Mar 14, 2019

I haven't built a Nuxt app before, but I'm in the middle of teaching these topics at Vue Mastery. Here are my feelings:

  1. I thought it was an intelligent convention that asyncData merges the return value into data. This felt like a nice shortcut and feature which this now removes. Now I need to remember to define my object in the data property. I agree with you though, that making this mirror the simplest Vue functionality is likely the best path for beginners.

This makes me wonder if you planning on keeping asyncData with the same functionality? This might be nice for those who want to upgrade to Nuxt 3 who are using it.

  1. I thought it was an intelligent convention that Nuxt (on the client side) waited for my API call to finish before loading my component. Given the built-in progress bar It works exactly as you'd expect. IMHO this is the optimal user experience, i.e. the new page showing up completely rendered and not in a loading state.

If your goal is to optimize for beginners, not having to deal with a loading state on the client side was quite nice. However, I read above that this is not possible?

Also, it might be nice to keep asyncData with the same functionality (of waiting for a return). Again, this might be nice for those who want to upgrade to Nuxt 3 who are using it.

@Atinux

This comment has been minimized.

Copy link
Member Author

commented Mar 14, 2019

Thanks @Gregg for your feedback.

We plan to have a Nuxt module to keep the current asyncData behaviour when migrating from Nuxt 2 to Nuxt 3, you can see my current implementation (POC) here: https://github.com/nuxt/nuxt.js/blob/feat/async-data/examples/v3/async-data/modules/nuxt-async-data/plugins/async-data.js

I wanted to get closer to Vue core by keeping only one data to:

  • Avoid splitting component data into 2 hooks (data + asyncData)
  • Support autocompletion in editors (not possible with asyncData)
  • Make the transition from Vue apps to Universal apps with Nuxt easier

I tried many ways to keep the same behaviour when navigating on client-side (ie: wait for all fetch calls before switching to the new route):

  • Overwriting the render function of the component while fetch is being called
  • Adding enter and leave hook into the <transition> component of <nuxt-child> to force waiting

But these two were breaking other stuffs inside Vue internals (page transitions, keep-alive, etc) 😢

@ttquoccuong

This comment has been minimized.

Copy link

commented Mar 14, 2019

Hi @Atinux , i working on quasar-framework and i see have a same thing in quasar framework.

https://quasar-framework.org/guide/app-prefetch-feature.html

so, how about it?

@Atinux

This comment has been minimized.

Copy link
Member Author

commented Mar 14, 2019

Hi @ttquoccuong

Actually, the prefetch feature of Quasar is the same as the current fetch of Nuxt.js: it's called during router.beforeEach and you cannot have access to the component instance inside it.

@henriqemalheiros

This comment has been minimized.

Copy link

commented Mar 14, 2019

First of all, I love this and it will definitely be a huge improvement for most of my projects. But there are some projects which are simpler and that kind of client-side UX might be too much overhead. Maybe the data being fetched is really small that there wouldn't be significant perceived speed improvement and it would cause an annoying flickering of the placeholder as the data is fetched really fast.

It's also a huge breaking change, as the whole Nuxt data fetching logic revolves mainly around asyncData. The fetch hook is a new way of thinking about data fetching and most users would only really benefit from it if they split their asyncData into several fetch hooks (just like the separation between post content and post author in the example).

The asyncData module looks promising, but it gets in the way of the "avoid splitting component data into 2 hooks" principle (also, autocompletion would be nice). I want to use fetch, but I don't want to rethink data fetching for now, at least.

So, my suggestion is to add a awaitFetch property on page components. If set to true, the fetch hook of that page component would be awaited before changing routes, just like today's asyncData. The fetch hook on other components don't need to be awaited.

Defaulting awaitFetch to true, would allow a smooth migration from v2 to v3, since users can simply refactor their asyncData into fetch, keeping the current behaviour and incrementally adopting the new behaviour as they add fetch on other components or set awaitFetch to false. Then, maybe, the awaitFetch could default to false on v4 to encourage the new behaviour.

@Atinux

This comment has been minimized.

Copy link
Member Author

commented Mar 14, 2019

That was how I wanted to implement it @henriqemalheiros, exactly like this to have no breaking changes and a smooth upgrade to all users...But sadly Vue.js does not have any asynchronous hook before rendering the component data.

I tried to hack <router-view> but without success, I am waiting for @posva expertise to see what we can do to support the current Nuxt behaviour by default while having the new fetch (accessing this inside the hook).

@pi0 pi0 pinned this issue Mar 14, 2019

@henriqemalheiros

This comment has been minimized.

Copy link

commented Mar 15, 2019

I was trying to find a way to achieve this and, from what I've found, it seems that the only way this could be done tidily would be through asynchronous lifecycle hooks. In the past, Evan said that there's hasn't been enough substantial benefits that justified implementing this feature. Maybe, with Nuxt on the scene, they could implement it in Vue 3?

One possible alternative solution would be tackling this question (small repro here). vue-router's inner workings were completely obscure to me until today and I haven't tried to code anything but, after some thought, it seems that if we could link the route instance registration (here and here) to beforeRouteEnter's callback function pooling, we could defer the route update function (which seems to actually update the component being rendered by router-view) and add the future awaited route as an another property (something like this.future, available via $router.futureRoute). But for this to work, router-view render logic would need to be rethinked, since it would need to register the future route instance too. I think this could be achieved with two slots, one for the current route and other for the future route, and a conditional render between the two. Again, this is just plain speculation and if I can find some spare time, I'll try it.

@davestewart

This comment has been minimized.

Copy link

commented Mar 26, 2019

Hello.

I've just seen issue this referenced in this issue I commented on...

One possible alternative solution would be tackling this question (small repro here)

...so perhaps I'll add my thoughts (though they may not be particularly relevant).

In brief, that issue relates to what might be loosely termed a "race condition" regarding fetched data and rendered component:

  • data is fetched in beforeRouteEnter
  • the component is created when next() is called
  • the component is then mounted
  • the fetched data is then set in the a callback next(vm => ... )

The issue is that because the data has not been made available in mount, both template and computed properties require v-ifs, conditionals or empty data to prevent errors.

I'm not so familiar with nuxt (just a couple of practice projects) but it seems that this proposal does not look to solve that, as the component is rendered first?

It seems to me that some way to merge the data before mounting would be the key to cleaner templates and no nextTick() funkiness or conditional cruft.

I made some suggestions in my comment on how this might be achieved, but as @henriqemalheiros noted, the code in Vue Router seems very complex / abstracted so it's something that would certainly need attention from @posva. I forked the repo and had a good dig about – to no avail!

@henriqemalheiros

This comment has been minimized.

Copy link

commented Mar 26, 2019

@davestewart your solution is good, but it achieves the same UX as the current Nuxt version. The problem we're discussing is accessing the component's instance before the route changes. Currently, vue-router does this:

  • beforeRouteEnter
  • beforeCreate
  • created
  • beforeMount
  • mounted
  • next() callback defined on beforeRouteEnter

If we could move the next() callback right after the created hook, we would have access to the component instance and somehow defer its mounting until some async data is fetched. So I suggested using slots in router-view or even (thinking about it later) something similar to the transition component. I tried messing around with things a bit, but with no success. It kind of works in the simplest scenario, but it's very brittle and falls apart quickly. I think the $futureRoute is the way to go, the problem being how to properly handle it in router-view.

@davestewart

This comment has been minimized.

Copy link

commented Mar 26, 2019

Yes, we're looking to solve the same problem.

My proposal looks to similar to asyncData() as you mention:

beforeRouteEnter
  data = getData()    <-- get the user data 
  next(data)          <-- I suggest passing `data` to next() here
                      <-- which will be merged by vue or vue router here
beforeCreate
                      <-- or maybe here
created
                      <-- you want to execute the callback here (which works for more functionality)
beforeMount
mounted
callback()            <-- current place callback is executed

https://jsfiddle.net/tsyav1up/2/

You mention:

if we could move the callback() right after the created hook, we would have access to the component instance and somehow defer its mounting until some async data is fetched"

The bits I don't understand:

  1. Why does mounting need to be delayed when we already got the data in beforeRouteEnter?
  2. Where does the extra complication with future routes and slots and so on come from?

Perhaps this is Nuxt internals I don't understand...

And ignore this if it's hijacking the RFC.

@henriqemalheiros

This comment has been minimized.

Copy link

commented Mar 26, 2019

@davestewart you're using getData() as an external function that relies in the route params, like this.

That's exactly what we currently have in Nuxt and that is not what this RFC is about. This RFC is introducing a new way to handle data fetching that is closer to what major SPAs do, like Facebook or YouTube. It also supports access to this, which is awesome in so many ways. It introduces a new DX and a new UX, the later being a breaking change.

As @Atinux said:

I tried to hack <router-view> but without success, I am waiting for @posva expertise to see what we can do to support the current Nuxt behaviour by default while having the new fetch (accessing this inside the hook).

So we want to use getData() as an internal function that relies on the component instance, like this (data fetching depends on the path prop). This way we could benefit from the new DX this RFC introduces without worrying about the breaking change in the UX.

@AndrewBogdanovTSS

This comment has been minimized.

Copy link

commented Jul 9, 2019

This is the topmost feature I wait for in Nuxt 3 😉

@bf

This comment has been minimized.

Copy link

commented Jul 15, 2019

I need this feature :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
7 participants
You can’t perform that action at this time.