2.6.10
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.
serverPrefetch() should reflect rendered and updated data variables when rendered on SSR
serverPrefetch() doesn't update the rendered view with new data after being called
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?
https://codesandbox.io/s/k289jxmw77 see index.vue
file
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 serverPrefetch
s 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 serverPrefetch
s 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.
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.
@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 serverPrefetch
s in the children components in the conditional in the example? You can't wait for all possible serverPrefetch
s 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.
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:
What cannot happen:
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
serverPrefetch
promises (unlike only it's own)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:
serverPrefetch
is collectedserverPrefetch
s, which means the one from AThere 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 serverPrefetch
s 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:
serverPrefetch
is collectedserverPrefetch
s, which means the one from AserverPrefech
is collectedserverPrefetch
s, which means the one from BWe 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?
serverPrefetch
is collectedserverPrefetch
s, which means the one from ASo now we actually rendered A four times and B three times! And this can go on and on and on...
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 serverPrefetch
s 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:
home.vue
is importing appLayout.vue
and using it as a layout (slots, <template><app></app></template>
)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)appLayout
s child component header
receives the correct data (displayPicture
) since it is rendered afterwardshome.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?
Most helpful comment
To conclude: your steps 2 and 4 in your diagram are not technically possible.