Hydration problem with client-side authentication and computed property in Vue/Nuxt layout - vue.js

My app (vue/nuxt 3) stores the user authentication state in localStorage. As a consequence it is only available on the client and prerendered pages always show the unauthenticated content. Client will render the authenticated content as soon as it is aware of it. That's ok and accepted.
However, this does not seem to apply for computed properties. My whole layout depends on the authentication state, e.g. like this:
<template>
<div :class="computedClasses">
<slot />
</div>
</template>
<script setup>
const computedClasses = computed(() => ({
if ($someReferenceToStore.user.logged.in) {
return 'loggedin'
} else {
return 'anonymous'
}
}))
</script>
The problem is, that even though the user is logged in, the computedClasses is not updated to loggedin but the server generated anonymous is shown. How to solve this? How can I make the client update the computed property and overwrite the server rendered classes?
I know, I can wrap parts of my template that depend on the authentication state with <ClientOnly> to avoid hydration mismatches. Wrapping my layout with <ClientOnly> would basically disable any server rendering. Can I set a property of an element (the :class="...") to client-only?

My current solution is to use a ref that's being updated.
<template>
<div :class="computedClasses">
<slot />
</div>
</template>
<script setup>
const computedClasses = ref('');
onMounted(() => {
computedClasses.value = $someReferenceToStore.user.logged.in ? 'loggedin' : 'anonymous';
});
</script>
Depending on the use case, one might want to add a watcher to update the ref as well.

Related

Nuxt vue component doesn't re-render after ssr

I have a component in nuxt, ssr enabled
<template>
<div class="img">
<img class="max-h-full" :src="imageSrc" />
</div>
</template>
<script setup lang="ts">
import { useDarkModeMonitor } from "../composables/darkMode";
const isDarkMode = useDarkModeMonitor();
const imageSrc = computed(() => {
if (isDarkMode.value)
return "URL_TO_SOME_IMAGE";
return "URL_TO_ANOTHER_IMAGE";
});
</script>
The useDarkModeMonitor checks if the user's preferred theme is dark mode. Since it uses windows to check, it is not available while being rendered on the server side. In that case isDarkMode gives hardcoded value true.
The page shows up in the correct theme but the images are hardcoded to show the dark version. On the Vue dev tools it shows isDarkMode and imageSrc computed properly, but the image src is still the initial value from ssr.
How can I force the image part to rerender with the new imageSrc?
PS: If you have a better way of handling this kind of situation, please do share :)

Vue SSR with v-if. How to correctly perform hydration?

I have a problem with Vue SSR.
In my project I have a page called slug, where depending on the data received in asyncData, the appropriate component is mounted. It looks more or less like this:
<div>
<component-a v-if='type === a' />
<component-b v-else-if='type === b' />
<component-c v-else-if='type === c' />
</div>
<script>
export default {
asyncData({ store }) {
store.dispatch('product/fetchAsync')
},
computed () {
type () {
return this.$store.state.type
}
}
}
</script>
However, Vue is not able to perform SSR hydration.
Is there a possibility that this is due to v-if statement?
How to solve this correctly? The only thing I can think of is prefixes and making each component a separate page, without v-if. But the client would like to avoid this.
As some comments say, your html structure at server side (where ssr is happening) don't know if the conditions for rendering an item or other are true.
You can use v-show instead of v-if. The difference is that v-show will render that element but with no data. This way your html won't change and hydration is successful

Fetching data from parent-component after reload a route with vue and vue-router

I have a component showing multiple routes (Step1, Step2, Step3...) on after each other. I navigate and pass properties like
<router-view
#next-button-step1="
$router.push({
name: 'Step2',
params: { myprop: thisprop },
})
"
[...]
</router-view>
with the routes defined like
const routes = [
{
path: "/s2",
name: "Step2",
component: Step2,
props: true,
},
This works well, until I reload a page/route, because the the data is lost. So I kind of want to fetch the data from the parent, after the component is loaded.
One of my ideas was using the local storage, but this does not feel right.
I am a newbie to Vue and I would like to ask what's the best practice here. Is it vuex like described here Reload component with vue-router? I'd appreciate a hint.
If localStorage is suitable depends on the use case.
You say that data is lost when you reload the page/route. It's perfectly possible to store this data to prevent data-loss on route change/reload. But changing/reloading the page will only be solved either by storing data in localStorage (and retrieving from localStorage on page init), or storing it on the server and retrieving it on page init...
Vuex may help you with the route change/reload part, but even Vuex won't help you with the page change/reload.
I will show you an example of how to save the data for the first case: route changes and reloads, because this may be achieved without adding Vuex. We will do this by having the data inside a parent component and passing this data to our routes. When a route changes the data it should emit an update-event (including the updated data) and the parent should store the changed data.
I'll show an example scenario in which the parent holds all the data. The routes
are responsible for editing different aspects of the data. Each time i switch between a route the parent supplies the data.
A working example can be found here: https://jsfiddle.net/b2pyv356/
// parent component / app.vue
<template>
<div>
<router-view
:state="state"
#updatestate="updateState"
></router-view>
<pre>Parent state: {{state}} </pre>
</div>
</template>
<script>
export default {
data() {
return {
state: {
name: 'name',
lastname: 'lastname'
}
}
},
methods: {
updateState(state) {
this.state = state;
}
}
};
</script>
// page1.vue
<template>
<div>
Route 1: State is:
<pre>{{state}}</pre>
<div>
Name: <input v-model="state.name">
<button #click="$emit('updateState', state)">Save</button><br>
</div>
<router-link to="/page2">Next step</router-link>
</div>
</template>
<script>
export default { props: ['state'] }
</script>
// page2.vue
<template>
<div>
Route 2: State is <pre>{{state}}</pre>
Name: <input v-model="state.name">
<button #click="$emit('updateState', state)">Save</button><br>
<router-link to="/">Previous page</router-link>
</div>
</template>
<script>
export default { props: ['state'] }
</script>
Persisting data:
On updateState you could store the data in localStorage
You could store some data in the request url ($router.params) or page query string. This has limits: some browsers enforce limits on how long a url may be. You are also responsible to validate/sanitize incoming data, do not trust that it won't be tempered with. Same applies to localStorage data btw. Common cases include storing a search query: if you refresh the page you still have the search query.
Let the backend save the data and retrieve the user's data on page load.

Axios request for one component on the server side

How is it possible to do in a vue component an axios request on the server side. The component should show the results from the axios response (it's a large response, so the server side should be more performant that the client side).
<template>
<div>
</div>
</template>
<script>
export default {
props: {
...
},
...
}
</script>
I've tried the lifecycle methods created and mounted, but both are running on the client side (and the created one also on the server). Is it possible to do some axios requests for one component on the server side?
You have two ways to do it in Nuxt.
You can user the asyncData method:
<template>
<div>
</div>
</template>
<script>
export default {
props: {
...
},
async asyncData (context) {
// here you have access to app, store, axios if you use it, etc
return yourData // your data will be available as a data
}
...
}
</script>
You also have the possibility to use middleware. It works the same way that the asyncData method and you can also use it in a page component, but you can use it in a global way so it can be executed at every page change (useful to redirect the user under certain conditions for example)
More infos: https://nuxtjs.org/api/pages-middleware/

Wrong component structure - prop-problems

I'm a noob and I'm trying to get my head around vue.js. In a particular route I have a view that looks like this
<template>
<div class="section">
<h1 class="title">Maintenance</h1>
<app-tabs></app-tabs>
<div class="columns">
<div class="column is-one-third">
<app-taglist></app-taglist>
</div>
<div class="column is-two-thirds">
<app-form></app-form>
</div>
</div>
</div>
</template>
<script>
import Taglist from "#/components/Taglist.vue";
import Tabs from "#/components/Tabs.vue";
import Form from "#/components/Form.vue";
export default {
}
</script>
Now I do understand the basics of passing data from a component to a child component, but what I'm actually trying to do is to pass data from app-tabs to app-taglist and from app-taglist to app-form. I'm starting to think that I'm attacking this all wrong, but if I do change my structure so that app-taglist is a child of app-tabs and app-form is a child of app-taglist - I can't really make proper use of the simple bulma, responsive, styling ...?
BTW: all components is registered globally at this time.
What kind of aproach would you advise me to look into - keeping in mind that I'm a noob.
Once you start getting to the point where you need to handle passing data between multiple components then I would consider using Vuex in order have a global component state that is accessible from all of your components.
Basically you can then create a totally separate "store" to hold your app's state (data):
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
And then from within each component, you can:
Use "getters" to read data from the store
Use "actions" to dispatch async functions that will "mutate" the store
e.g:
store.commit('increment') // this is a mutation
console.log(store.state.count) // -> 1