Vue.js recalculate computed properties on client after server rendering - javascript

I've got this simple vue single file component
<template>
<v-layout>
<v-flex xs12 sm6 offset-sm3>
<v-card v-bind:color="color" class="white--text">
<v-card-title primary-title>
<div>
<h3 class="headline mb-0">Kangaroo Valley Safari</h3>
<div>{{card_text}}</div>
</div>
</v-card-title>
</v-card>
</v-flex>
</v-layout>
</template>
<script>
import MessageCard from '../components/MessageCard.vue';
const colors = [
'red',
'pink',
'purple',
'indigo',
'green'
];
export default {
data () {
return {
card_text: 'Lorem ipsum dolor sit amet, brute iriure accusata ne mea. Eos suavitate referrentur ad, te duo agam libris qualisque, utroque quaestio accommodare no qui.'
}
},
computed: {
color: function () {
const length = colors.length;
return colors[Math.round(-0.5 + Math.random() * (length + 1))] + ' darken-3';
}
},
components: {
MessageCard
}
}
</script>
The problem is that by server-side render I am getting computed color of v-card as a style, but when the client side hydration starts computed property recalculates which changes the style and causes rerender.
Of cause, I can fix it fix tag, but I'm curious is there some other ways to make it work correctly.

Computed properties are always reevaluated during client side hydration. It is generally not a good idea to relay on side effects in your computeds (like Math.random()), since Vue.js expects computed properties to be idempotent.
So usually you would calculate that random value once at creation and store it as data. However the data of a component is also not preserved between SSR and hydration.
So a way you could solve this, would be by storing the random value in the state of a Vuex store. It is then possible to restore the state of the store form the server in the client.

Related

What causes this "maximum call stack size exceeded" error?

I am working on a Vue 3 and Bootstrap 5 app. I needed a date-picker and I choose Vue 3 Datepicker.
In components\Ui\Datepicker.vue I have:
<template>
<datepicker
#selected="handleSelect"
v-model="dateSelected"
:upper-limit="picked_to"
:lower-limit="picked_from"
class="datepicker text-center" />
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const dateSelected = ref(new Date());
return {dateSelected}
},
methods: {
handleSelect() {
this.$emit('setDate')
}
}
}
</script>
In components\Ui\Navigation.vue I have:
import Datepicker from './Datepicker'
export default {
inject: ['$apiBaseUrl'],
name: 'Navigation',
components: {
Datepicker,
},
data() {
return {
// more code
}
},
methods: {
setDate() {
this.$emit('setDate');
}
},
}
In components\Content.vue I have:
<template>
<div class="main">
<div class="d-sm-flex>
<h1>{{ title }}</h1>
<Navigation
#setDate='setDate'
/>
</div>
<div class="content">
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat.</p>
</div>
</div>
</template>
<script>
import Navigation from './Ui/Navigation'
export default {
inject: ['$apiBaseUrl'],
name: 'Content',
components: {
Navigation,
},
props: {
title: String,
},
emits: ['setDate'],
data() {
return {
headers: {
'content-type': 'application/json',
'Accept': 'application/json'
},
from: '',
to: '',
}
},
methods: {
sendData() {
this.axios.post(`${this.$apiBaseUrl}/submit`, this.fields, {options: this.headers}).then((response) => {
if (response.data.code == 200) {
this.isReport = true;
}
}).catch((errors) => {
this.errors = errors.response.data.errors;
});
}
},
setDate() {
console.log('Date');
},
}
}
</script>
The problem
Although I select a date from the datepicker, the setDate() method is not executed. The Chrome console shows instead:
Maximum call stack size exceeded
Where is my mistake?
As comments have mentioned previously, the error usually occurs when having an infinite loop.
As Kissu pointed out this can easily happen if you have an event that is emitted to a parent component which then changes data that is passed as props to a child component which then triggers an event to the parent and so on.
In the code you showed I can't really find any loops, they might be in the parts you omitted.
Edit:
The infinite loop could actually be caused by the naming of your component.
You're using <datepicker ...> inside Datepicker.vue without registering the Datepicker of vue3-datepicker there explicitly.
That probably causes vue to recursively try to mount the component in itself causing the maximum stack trace error, but that's just a guess.
But there's a couple issues in your code still.
First off:
<template>
<datepicker
#selected="handleSelect" // <-- according to the docs there are no events that the datepicker emits
v-model="dateSelected"
:upper-limit="picked_to" // <-- where does the picked_to value come from?
:lower-limit="picked_from" // <-- where does the picked_from value come from?
class="datepicker text-center" />
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const dateSelected = ref(new Date());
return {dateSelected}
},
// do not mix options & composition api
methods: {
handleSelect() {
this.$emit('setDate')
}
}
}
</script>
You're mixing options and composition API, which is never a good idea.
There's also a couple values that seemingly come from nowhere and you're listening to a #selected event from the datepicker, which according to the docs doesn't exist, so you'll have to watch for changes in the selected date yourself.
The same component in the composition API would look like this:
<template>
<Datepicker v-model="initialDate" class="datepicker text-center" />
</template>
<script>
import Datepicker from "vue3-datepicker";
import { ref, watch } from "vue";
export default {
components: { Datepicker },
setup(props, { emit }) {
const initialDate = ref(new Date());
watch(initialDate, (newDate) => {
emit("setDate", newDate);
});
return { initialDate };
},
};
</script>
I tried to recreate the example you gave in a sandbox and I do not encounter any infinite loop issues.
You might wanna check it out and compare with your code and possibly fix all the other issues first and see if that helps your situation :)

Need help to pass data from page to dynamic page

I'm creating a website to showcase products using NextJs. I need to pass data from a page to a dynamic page. All the data is a json array of objects located in a data folder inside the project. I want to list all objects in pages/rolls/index.jsx, and when an objects corresponding "Learn More" Link button is clicked, takes me to a more detailed version of the page. I can't make it work using next/link and useRouter, so here's what I did.
My project structure is:
- data
- rollData.json
- pages
- rolls
- [rolldetail].jsx
- index.jsx
// rollData.json
[
{
"name": "8\" x 0.080\" x 300'",
"price": 300,
"product_id": "PL8080300",
"image": "/8flat.webp",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
},
{
"name": "12\" x 0.080\" x 300'",
"price": 450,
"product_id": "PL12080300",
"image": "/12flat.webp",
"description": "Ac turpis egestas sed tempus urna et pharetra"
}
]
// rolls.jsx
import rollsdata from '../../data/rollData.json'
const Rolls = () => {
return (
<main>
{rolls.map((roll) => {
return (
<div key={roll.product_id}>
<h2>{roll.name}</h2>
<h3>
Description
</h3>
<p>{`${roll.description.slice(0, 163)}.`}</p>
<div>
<span>Price</span>
<span>{`$${roll.price.toFixed(
2
)}`}</span>
</div>
<Link href={`/rolls/${encodeURIComponent(roll.product_id)}`}>
<a>click</a>
</Link>
</div>
)
})}
</main>
)
}
// [rollDetail].js
import { useRouter } from 'next/router'
import rolls from '../../data/rolls.json'
const RollDetail = () => {
const router = useRouter()
const roll = rolls[router.query.rolldetail]
console.log(roll)
return (
<div>
<h2>Roll detail</h2>
<p>{roll.name}</p>
<p>{roll.description}</p>
</div>
)
}
export default RollDetail
And it gives me the error:
Uncaught TypeError: roll is undefined
I don't understand how to use router to display a specific roll object, can someone tell me what I'm doing wrong and how to achieve it please? Should I be using getStaticProps and getStaticRoute?
Actually by default all the routes either they are dynamic or static will be statically pre-rendered during build time of app, unless you use getStaticProps, getInitialProps or getServerSideProps...
But during development all the pages pre-render every time you hit the request to specific url, no matter what you have used...
Now lets back to actual question, your dynamic pages will be rendered twice first time you are getting undefined as your url parameters but on second render your url parameters will be availble, so you can simply put checks to check either url parameters are undefined or not...
This is happening due to some reason, checkout this link => click

How to render a v-for based on changing props?

Maybe I have a missconcept based on React.js, but I am working wih Vue.js
I Just create a component:
<ResultCards v-bind:cards="cards"/>
cards is updated when an event is triggered by other component:
methods: {
fillResultCards(cards){
this.cards = cards;
}
}
This is my ResultCard Component
<template>
<div>
<v-card
v-for="card in cards"
v-bind:key="card.id"
>
// ..card detail here
</v-card>
</div>
</template>
<script>
export default {
name: 'ResultCards.vue',
props: {
cards: Array,
},
};
</script>
But when I update the "cards" props send me the message:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "cards"
I tried to copy cards prop to a internal dat
data() {
return { internalCards: this.cards };
}
but it doesn't works.
Okay, it works, maibe I had a typo, but this is the FULL flow.
I have external Component (a search bar)
<SearchGit v-on:item-selected="fillResultCards"/>
when search concept fill the input, it emit the event item-selected passing results (from API) as args.
this.$emit('item-selected', this.cache.filter(item => item.full_name === textSearch));
In Container I set my data with the function
data: () => ({
cards: [],
}),
methods: {
fillResultCards(cards){
this.cards = cards;
}
}
then this "new version" of cards pass as prop to the render component
<ResultCards v-bind:cards="cards"/>
And the render component only need to render data, and thats all:
<v-card
v-for="card in cards"
v-bind:key="card.id"
>
<v-list-item three-line>
<v-list-item-content>
<div class="overline mb-4">{{ card.name }}</div>
<v-list-item-title class="headline mb-1">{{
card.full_name
}}</v-list-item-title>
<v-list-item-subtitle>{{ card.description }}</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-avatar tile size="80" color="grey">
<img v-bind:src="card.owner.avatar_url" alt="avatar" />
</v-list-item-avatar>
</v-list-item>
</v-card>
I don't change any of the code, but maybe a cache or something keeps old changes, it works now.

Using Vuetify how can I place a search bar over a carousel?

As the title says, I want to place a search bar functionality over a carousel and I'm trying to achieve this using <v-autocomplete> and v-carousel>.
Using the code snippets from the official Vuetify docs, I have managed to have this:
<template>
<v-layout
justify-center
app
>
<v-flex
xs12
sm6
>
<v-container
fluid
grid-list-md
>
<v-layout
row
wrap
>
<!--Carousel-->
<v-flex xs6>
<v-carousel
hide-controls
hide-delimiters
height='200'
interval='2500'
>
<v-toolbar
dark
color="teal"
>
<v-toolbar-title>State selection</v-toolbar-title>
<v-autocomplete
v-model="select"
:loading="loading"
:items="items"
:search-input.sync="search"
cache-items
dense
hide-no-data
hide-details
label="What state are you from?"
solo-inverted
>
</v-autocomplete>
<v-btn icon>
<v-icon>more_vert</v-icon>
</v-btn>
</v-toolbar>
<v-carousel-item
v-for="(item,i) in items"
:key="i"
:src="item.src"
>
</v-carousel-item>
</v-carousel>
</v-flex>
</v-layout>
</v-container>
</v-flex>
</v-layout>
</template>
<script>
<!--I use selective imports. This might not be needed for you when trying to reproduce it -->
import VContainer from "vuetify/lib/components/VGrid/VContainer";
import VFlex from "vuetify/lib/components/VGrid/VFlex";
import VLayout from "vuetify/lib/components/VGrid/VLayout";
import VCard from "vuetify/lib/components/VCard/VCard";
import VImg from "vuetify/lib/components/VImg/VImg";
import VCarousel from "vuetify/lib/components/VCarousel/VCarousel";
import VAutocomplete from "vuetify/lib/components/VAutocomplete/VAutocomplete";
export default {
name: "HomeContents",
data: () => ({
loading: false,
items: [],
search: null,
select: null,
states: [
'Alabama',
'Alaska',
'American Samoa',
'Arizona',
'Arkansas',
'California',
'Colorado',
'Ohio',
'Oklahoma',
'Oregon',
'Washington',
'West Virginia',
'Wisconsin',
'Wyoming'
]
}),
watch: {
search (val) {
val && val !== this.select && this.querySelections(val)
}
},
methods: {
querySelections (v) {
this.loading = true
// Simulated ajax query
setTimeout(() => {
this.items = this.states.filter(e => {
return (e || '').toLowerCase().indexOf((v || '').toLowerCase()) > -1
})
this.loading = false
}, 500)
}
},
components: {
VContainer,
VFlex,
VLayout,
VCard,
VImg,
VCarousel,
VAutocomplete
}
}
</script>
The problems I am facing are the following:
How can I lose the teal toolbar and have just one simple single line to use for searching, that will be positioned in the center of the carousel?
Whenever I click on the search bar, I see this: [object Object]. How do I get rid of this?
With my current implementation, every time I'm searching for a state I'm able to successfully find it but the problem is that the carousel background transition effect stops. So for example, If I search for "Oklahoma" I can see the result but the background carousel transition stops. How can this be fixed?
I'll tackle your questions as you've put them.
1: The teal color is specified in your declaration of v-toolbar.
<v-toolbar
dark
color="teal"
>
You can set this color to anything you want, rgb or hex, so if you want it to be transparent you could do this.
<v-toolbar
dark
color="rgba(0,0,0,0)"
>
To specify the position of the toolbar comes down to styling. You can adjust the default vuetify styles but for a single item like this the easiest way to set styling that will override the default is to use inline styling, like this:
<v-toolbar
dark
color="rgba(0,0,0,0)"
style="position:absolute;top:75px;z-index:400;"
>
As you've set the height, you can position the toolbar to center it. The z-index may be necessary to place it above the carousel. In here you could also style the text color, width etc.
2: In your posted code the reason you are getting [object Object] is because the v-autocomplete is trying to iterate through 'items' which is empty. You have 'states'. Set items like this: :items="states" To get this working as expected I set it up like this:
<v-autocomplete
style="background:rgba(0,0,0,0)"
v-model="select"
:items="states"
:loading="isLoading"
:search-input.sync="search"
color="white"
hide-no-data
hide-selected
item-text="Description"
item-value="state"
label="States"
placeholder="Start typing to Search"
prepend-icon="mdi-database-search"
return-object
>
</v-autocomplete>
Which is straight from the vuetify docs.
3: I can't reproduce the carousel pausing, it continues throughout on mine.
Hope this helps somewhat.

How is it possible to interpolate a JavaScript variable based on React state into another variable? [duplicate]

This question already has answers here:
Accessing an object property with a dynamically-computed name
(19 answers)
Closed 4 years ago.
I have a React component pulling from a file with images, copy, and other data needed for the app. The issue I'm coming up against is simply syntactical: how can I reference this data based on the state of the component?
data.js looks like this (simplified version):
import changingAgents from '../public/images/changing-agents.jpg';
import cta from '../public/images/cta.jpg';
import logo from '../public/images/logo.jpg';
export const images = {
changingAgents,
cta,
logo,
}
export const copy = {
services: {
header: 'What are you looking for?',
firstTimeInvestor: {
title: 'First-Time Investor Service',
intro: 'At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque.',
},
changingAgents: {
title: 'Changing Agents',
intro: 'At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti.',
},
}
And here are the relevant parts of the component, Services.js:
import { copy, images } from '../../data';
export default class Services extends Component {
state = {
segmentTrigger: 'changingAgents',
}
changeSegmentTrigger(chosenSegmentTrigger) {
this.setState({segmentTrigger: chosenSegmentTrigger})
}
render() {
return (
<div className="services">
<h2>{copy.services.header}</h2>
<Section backgroundImage={this.state.segmentTrigger} className="services__segment-triggers" >
<div className="services__segment-triggers__select">
<Button color="yellow" onClick={() => this.changeSegmentTrigger('firstTimeInvestor')} label="First-Time Investor" />
<Button color="yellow" onClick={() => this.changeSegmentTrigger('changingAgents')} label="Changing Agents" />
</div>
<div className="services__segment-triggers__info">
{/* Insert header here! */}
</div>
</Section>
</div>
)
}
}
Basically what I need to do is add text that will change depending on this.state.segmentTrigger in <div className="services__segment-triggers__info">. I tried a few variations to no avail, such as:
<h2>{copy.services.${this.state.segmentTrigger}.title}</h2>
<h2>{copy.services.${this.state.segmentTrigger}.title}`
<h2>{copy.services.this.state.segmentTrigger.title}</h2>
Is this possible? I get syntax errors for the first two, and the third one just doesn't work (which seems obviously logical to me, it was a bit of a Hail Mary even trying it, as of course there is no key named this in the copy.services object).
The answer you're looking for is
<h2>{copy.services[this.state.segmentTrigger].title}</h2>
The reasoning is that you're trying to access a key of copy.services, but that you will not know what key you're trying to access until the user starts clicking buttons, and which one you access will be dynamically decided.
The way objects and keys work in JavaScript, is that the key is technically a string, so using this example:
let x = {
test1: 4,
test2: 5,
'test3': 6
};
x.test1 // is 4
x.test2 // is 5
x.test3 // is 6
x['test1'] // is 4
x['test2'] // is 5
x['test3'] // is 6
const y = 'test1';
x.y // undefined. we never set a key of y.
x[y] // this is interpreted as x['test1'] so the result is 4

Categories

Resources