Vue: serverPrefetch doesn't work as expected

Created on 22 Apr 2019  ·  33Comments  ·  Source: vuejs/vue

Version

2.6.10

Steps to reproduce

Suppose we have a component:

export default {
    data(){
        return { foo: null}
    }
    async serverPrefetch(){
        this.foo = 'serverPrefetch'
    },
    created(){
        this.foo = 'created'
    },
}

foo is initially null, then created() updates it to created and this reflects in the html interpolitatiion, but when serverPrefetch() updates this.foo it is not reflected on the rendered html, despite being called afterwards.

What is expected?

serverPrefetch() should reflect rendered and updated data variables when rendered on SSR

What is actually happening?

serverPrefetch() doesn't update the rendered view with new data after being called

Most helpful comment

To conclude: your steps 2 and 4 in your diagram are not technically possible.

All 33 comments

Changes to state/data in serverPrefetch() should retrigger a VNode re-render for the affected components watching the reactive data

The component isn't rendered before all serverPrefetch lifecycle hooks are called. See the passing test.

Do you have a runnable reproduction?

It seems that it is sending the correct content over on the server-side-rendering, but it's not replacing the correct state when it mounts. How do we ensure this happens?

You need a solution to store the data resolved on the serve in the rendered HTML page so that the client can pick it up in the browser-side JS.

https://ssr.vuejs.org/guide/data.html#data-store
https://slides.com/akryum/vue-26-ssr-revolution#/21

@yyx990803 I can't close the issue 😸

I figured out the issue. Vue.observeable doesn't trigger a re-render on server components when they import a child component which created the Vue.observeable

It's pretty complex, but can someone clarify whether components are re-rendered after serverPrefetch updates a Vue.observable that multiple components rely on. It seems not to be happening in my case

Reactivity is entirely disabled during SSR, so there will be no "updates" - the state can be mutated, but it must happen before the render function is called.

This means that global state reactivity between different components is essentially disabled. Can we relook at this, or perhaps propose an RFC for 3.0?

It seems the render function for each component is being called before other components serverPrefetch is.

Shouldn't all serverPrefetchs for all components in the components: {...} be called before any component is actually rendered?

is this expected behaviour @yyx990803 @Akryum, wouldn't it be more practical and expected for all the serverPrefetchs to be called before the render functions? especially when in the ssr vue examples state that the vuex store can be updated with serverPrefetch

https://ssr.vuejs.org/guide/data.html#data-store
https://ssr.vuejs.org/api/#serverprefetch

otherwise the client app would render using different state and the hydration would fail.

while the hydration isn't failing, the server is rendering a different state (because of the reasons described in the above comment). Is this what you'd expect serverPrefetch to function like?

In my use case, index.vue is using layout.vue as a component (wrapping index.vue in app.vue's tag,s since app.vue is the "parent" component, it generates all the necessary global state mixins (as to prevent code duplication). app.vue has it's own component, header.vue (which is rendered after app.vue and receives the correct state.

What's happening in order is:
import index
import app
import header
index.serverPrefetch()
index.render()
app.serverPrefetch()
app.render()
header.serverPretch()
header.render() (this recieves correct global state from app.vue as it is rendered afterwards)

In my opinion, what should be happening is:
import index
import app
import header
index.serverPrefetch()
app.serverPrefetch()
header.serverPretch()
index.render() //recieves same global state as all components
app.render()
header.render()

Shouldn't all serverPrefetchs for all components in the components: {...} be called before any component is actually rendered?

That is not possible, because children component instances are not created before render is called.

@DominusVilicus the code you originally posted is working as expected (per test case). By design, a component's serverPrefetch should only affect data of its own (or child components via passed props). The only case that it "doesn't work" is that when a component's serverPrefetch is mutating state that doesn't belong to itself.

Consider a simple example where the component conditionally renders one component or another depending on its state:

<template>
  <MyComponentA v-if="showA"/>
  <MyComponentB v-else/>
</template>

Now you can see we can't create the component instances of MyComponentA nor MyComponentB because we don't know which one will be created before actually rendering the current component.

Great points, I understand why now.

Evan is correct that the case doesn't work when using global state.

I guess this is why Vuex attaches itself to root component in nuxtjs, whereas my example attaches to app which is after root and page/index.vue. When passed to root it passes all the data to subcomponents

But why do the docs give examples of child components updating global state?

I totally understand your reasoning behind it though, just unsure how to proceed with my use-case

While i'm not well versed in SSR(it all seems like magic to me) I will interject this:

I believe any data collection SHOULD occur prior to a component being rendered
to me, both server side and client side...the model should be exactly the same
as soon as you provide inconsistencies, you provide surface area for bugs

@DominusVilicus You should update the global state in a component which is a parent of all the components requiring it.

@hybridwebdev You can't cover all the cases with that, for example components fetching their own data, like with Apollo GraphQL. 😸 But you can still do that, like before serverPrefetch, with router.matchedComponents (with the old limitations: only works on route-level components, no access to this).

Well, I think we’ve discussed all the points there is to discuss, but the final call is up to you guys. I’d be happy for a RFC, or to provide a PR, but only if there’s a shot it’ll pass.

@DominusVilicus I don't think it's technically feasible without doing multiple re-render, which would be very bad performance-wise.

d5hwu7qQddLYno7Is2fZs1ZtPfU4832ZoOZ9gaTV
Would you consider this optimal for a production scenario? probably not. (correct data is updated when the component mounts, but the server-side component should also have the correct data)

code used (min-rep example)

<template>
   <appLayout><!-- on a side note, since appLayout creates the state, and then the header component is rendered after app.serverPrefetch(), the header component receives the correct state ($state.me.displayPicture), which is why you can see the display picture in the corner, despite the index.vue file not receiving the correct state. (since it's rendered before app.serverPrefetch() is called. What bugs me, is the lack of consistency -->
        hello {{ $state.me.firstName }} 
    </appLayout>
</template>

Isn't the goal of SSR to provide and render the correct data and have parity with the client-side?

we all know why this is happening, Vue doesn't have reactivity on the server-side, which is understandable, and totally fine and expected.

as @hybridwebdev mentioned:

I believe any data collection SHOULD occur prior to a component being rendered

I think all data collection should occur prior to any component being rendered. yes, this only occurs when serverPrefetch is dealing with global component state, but this is a common use-case.

How I'd implement it

@Akryum's concerns about re-rendering & conditional components can be solved. It would just involve awaiting all serverPrefetch()s promises _as the component and subcomponents are compiled_ which notifies/alerts the relevant VNodes that depend on the data, and then finally rendering the VNodes (this way the entire app has the same global state and is more in-line with how SSR is expected to work)

This way, every component is only rendered once

I'd be glad to implement it if I got your approvals, but it's up to you guys, or an RFC

I think all data collection should occur prior to any component being rendered.

As I wrote in previous comments, this is achievable with router.getMatchedComponents which was the only way of data prefetching before serverPrefetch. Its functionality hasn't changed and you can still use it, although it has the same caveats/limitations as before (only route-level, no access to this).

It would just involve awaiting all serverPrefetch()s promises

What if you have serverPrefetchs in the children components in the conditional in the example? You can't wait for all possible serverPrefetchs without progressively rendering the App first, since for a component A, the direct children component instances of A are only created when A is rendered.

notify/alert the relevant VNodes that depend on the data, and then finally rendering the VNodes

Hum I think you may have a misunderstanding of what VNodes are. 😸 VNodes are created during render, not before. Basically VNodes are the parts that make up the Virtual DOM representation in memory created during render, when you call h (or the compiled template does).

This way, every component is only rendered once

I think that's technically not possible.

I'd be glad to implement

I've been thinking about the SSR data-prefetching problem for a year and came up with the current implementation of serverPreftch, but if you come up with a better implementation, it would be amazing!

Here are some slides about it: https://slides.com/akryum/vue-26-ssr-revolution#/21

Great diagram, it made me understand ssr further. I created another diagram (excuse my poor diagram) explaining how we could solve those caveats.

image

this is how the proposal would work (pseudocode)

import index
import app
    createGlobalMixin() //see app.vue
import header

/**
  * this reads and compiles the template, creates vnodes and checks whether conditional components
  * should be compiled (as to not call serverPrefetch on `v-if="false"`s)
  * it finds <app> and `app.compile()`s it, which finds <header> and `header.compile()`s it too
  */
index.compile()
app.compile()
header.compile()

//all VNodes exist, and notify/deps are attached 

index.serverPrefetch() //if provided (to add other page-specific local data state)
app.serverPrefetch() // adds some data to the $state object, which alerts all vnodes of updated data
header.serverPretch()

index.render() //recieves same global state as all components
app.render() //recieves same global state as all components
header.render() //recieves same global state as all components

I'll try to explain it in code

app.js

import index from "page.vue"

let app = new Vue({
   render: h => h(page)
})

export default app //then the server creates client and server bundles

page.vue

<template>
    <app>
        <p>{{ foo }} (works and prints 'notBar') </p>
        <p>{{ $state.user.firstName }} would work in my proposal, but currently returns null</p>
    </app>
</template>
<script>
import app from "app.vue"

export default {
    data(){
        return {
            foo: 'bar'
        }
    },
    serverPrefetch(){
        this.foo = 'notBar'
    },
    components: {
        header
    }
}
</script>

app.vue

<template>
   <div>
       <header/>
       <slot/>
   </div>
</template>
<script>
import header from "header.vue"

let state = Vue.observable({
   user: {
        firstName: null,
        displayPicture: null
    }
})

/**
 * Vue.mixin applies a mixin globablly to every component
 */
Vue.mixin({
    beforeCreate(){
        this.$state = state
    }
})


export default {
    serverPrefetch(){
        let user = api.call('user')
        this.$state.user = user
    },
    components: {
        header
    }
}

</script>

header.vue

<template>
   <img :src="$state.user.displayPicture"> <!-- can access state both in my proposal, and current because it's rendered after app.render() -->
</template>

ps, i've sent you a friend request on discord if you'd like to discuss this faster @Akryum

VNodes aren't created until step 6 on your diagram (Rendering).

this reads and compiles the template

Templates are already compiled to render functions:
https://vuejs.org/v2/guide/render-function.html
https://template-explorer.vuejs.org

creates vnodes

VNodes are created by render()

checks whether conditional components should be compiled (as to not call serverPrefetch on v-if="false"s)

This happens when the component is rendered.

all VNodes exist, and notify/deps are attached

Again, no VNodes in a component exist until this component instance is rendered. Children component instances and their data/deps will be created when the component renders.

Example:

- A
   |- B
   |- C

What happens:

  • A is created
  • A is rendered
  • B & C are created
  • B & C are rendered

What cannot happen:

  • A & B & C are created
  • A & B & C are rendered

Since B & C are children of A, creating B & C requires rendering A first.

Yeah, I get you, my idea is to modify render internally so that it

  1. render the vnodes
  2. awaits all of the serverPrefetch promises (unlike only it's own)
  3. and then finally render the vnodes to a string

This way there is no breaking changes, and serverPrefetch behaves more expectedly when behaving with global observeables.

I'll provide a PR request shortly if I can figure it out. I don't want to give you guys extra work on top of an already amazing package you are trying to work on.

- A
   |- B
   |- C

Ok, let's say you have serverPrefetch on both A, B & C, and that A, B & C all rely on different data from your API to correctly render. For example, A needs an API result to decide whether to render B or C with a v-if. B & C needs some other APIs calls to display data in their template too, and their serverPrefetch relies on some props passed by A.

What you are proposing will happen:

  • A is created
  • A renders
  • Neither B nor C are created because A doesn't have the necessary data from the API to decide which one to render
  • A's serverPrefetch is collected
  • Await all serverPrefetchs, which means the one from A
  • Generate the string representation of the app

There is a big issue here: neither B nor C where created, but you expected one of them to be created.

How to fix this? The only way is to render A again after the first serverPrefetchs awaiting, so that the VNode for B or C is created. So let's say we add a new rule: after the serverPrefetch is awaited, we re-render the component.

Here is what would happen:

  • A is created
  • A renders
  • Neither B nor C are created because A doesn't have the necessary data from the API to decide which one to render
  • A's serverPrefetch is collected
  • Await all queued serverPrefetchs, which means the one from A
  • A renders again
  • B or C is created, let's say B (v-if was true)
  • B renders
  • B's serverPrefech is collected
  • Await all queued serverPrefetchs, which means the one from B
  • B renders again
  • Generate the string representation of the app

We can already see we did multiple renders of the same component, which will affect performance. But it can get worse from there: what if B serverPrefetch actually mutated data that A depends on?

  • A renders again
  • A's serverPrefetch is collected
  • Await all queued serverPrefetchs, which means the one from A
  • A renders again
  • B is updated (prop)
  • B renders again
  • etc

So now we actually rendered A four times and B three times! And this can go on and on and on...


Takeaways

1 - It's impossible to have one and only one step with all the possible serverPrefetch in the app and call/await them all at once. The code can't guess if B or C would have been rendered and if we need to call their serverPrefetch. Also, if, like in the above example, B serverPrefetch relies on data passed via props by A, it can't work without having a intermediate step where A re-renders to pass the value via props to B.
2 - We can't render all the components in the app at once either after waiting for all the possible serverPrefetch (which is impossible, see takeaway 1). Also, component instances are not created until the parent component is rendered, so their serverPrefetchs don't even exist yet.
3 - Mutating state that a parent component depends on is a very bad idea when doing SSR since it can lead to multiple cascading updates. You don't want the HTML page to take ages to be generated and sent to the client.
4 - We can't call/await serverPrefetch in a component instance after rendering it, otherwise we have to render it multiple times (and even call its serverPrefetch again).

To conclude: your steps 2 and 4 in your diagram are not technically possible.

@DominusVilicus You should update the global state in a component which is a parent of all the components requiring it.

Let me rephrase it:

A component A should only mutate state that itself depends on OR state that its children component depend on. So for you use case, you should have a parent component which contains both index, app and header as children, and its serverPrefetch can mutate global state needed by index, app or header.

I've had long thought about this over the weekend and I understand the implications it would cause if child components caused updates. I think it may be worth considering adding the additional rerenders since they do change state, even if the child components do change it. I think this is what most Vue users would like.

what if B serverPrefetch actually mutated data that A depends on?

That's what is happening in the example image:
56849675-9a562c80-6936-11e9-8c4f-17d6b1933583

  1. home.vue is importing appLayout.vue and using it as a layout (slots, <template><app></app></template>)
  2. appLayout.vue updates the global store which home.vue depends on (updates user in serverPrefetch so then user fetch isn't repeated in every single component, as all users of appLayout.vue depend on the user state, including itself and it's children)
  3. as you can see, the appLayouts child component header receives the correct data (displayPicture) since it is rendered afterwards
  4. home.vue doesn't update its VNodes based on the updated data (because it seems there's no reactivity, but there is sometimes like in created() afaik)

On the client-side, everything would work correctly because of reactivity, but in the server-side, this isn't the case, but I believe it should be. Even if it involves rerenders

I will continue to think of a viable solution to sort this issue that wouldn't add too much extra overhead.

I think this is what most developers using Vue would want. If they are causing a prop/global state object to update in Vue then, of course, they'd want it to be reflected across the entire app.

Guys, I have problems to replace asyncData for serverPrefetch.
Hydrate doesn't happen. Show me a warning about the mismatch HTML the server for the client.

I try to update the footer (in parent) component using getters to get data. This data is set for serverPrefetch in Details (in child) component.

AsyncData (this same method in HN example ) await all promises, But serverPrefetch doesn't the same approach.

Please, show an example of this approach for update Parent component (with getters ) with serverPrefetch in the child with vuex

<App>
 <header>
 <router-view>
  <details>
</router-view>
<footer>
</App>

@EmilioAiolfi The mismatched HTML error is commonly associated with authoring incorrect markup. Have you double checked that the markup in your components is valid?

Was this page helpful?
0 / 5 - 0 ratings