Why normal objects become reactive automatically in Vue3? - vue.js

In this article there is an example. It says the normal object wont be 'reactive'.
I made a test in this codesandbox, and found that changes to the normal object (even the normal string) can automatically change the view.
<template>
{{ personName }} <!-- will change to Amy, why? -->
{{ person.name }} <!-- will change to Amy, why? -->
{{ personRef.name }}
{{ personReactive.name }}
<button #click="changeName('Amy')">changeName</button>
</template>
<script setup>
import { ref, reactive } from "vue";
let personName = "John";
const person = { name: "John" };
const personRef = ref({ name: "John" });
const personReactive = reactive({ name: "John" });
const changeName = (name) => {
personName = name;
person.name = name;
personRef.value.name = name;
personReactive.name = name;
};
</script>
How did this happen? Did I miss something in the Vue document?
I've tried the Vue SFC Playground which gives the same result.

The normal variables are not becoming reactive. What is really happening is a re-render that is caused by the changeName() method. The re-render is caused because you changed the values of the reactive variables. As a result, The DOM is updated with the latest values correctly shown for both normal and reactive variables.
To illustrate this I have created a fork of your sandbox with this code:
<template>
{{ personName }}
{{ person.name }}
{{ personRef.name }}
{{ personReactive.name }}
<button #click="changeReactiveName('Amy')">changeReactiveName</button>
<button #click="changeNormalName('Fanoflix')">changeNormalName</button>
</template>
<script setup>
import { ref, reactive } from "vue";
let personName = "John";
const person = { name: "John" };
const personRef = ref({ name: "John" });
const personReactive = reactive({ name: "John" });
const changeReactiveName = (name) => {
personRef.value.name = name;
personReactive.name = name;
};
const changeNormalName = (name) => {
personName = name;
person.name = name;
};
</script>
When you change only the non-reactive variables by calling changeNormalName(), the change (correctly) does not cause a DOM re-render and you will not see the changes reflected in the DOM.
Once you call the changeReactiveName() method, the DOM will be re-rendered to incorporate the changes done to the reactive variables. Hence, The DOM will also render the new values of the non-reactive variables which you updated previously and will show Fanoflix.
For a better understanding read how Vue handles rendering: docs

Related

Passing a value via prop, editing it and saving the new value vue 3

I am trying to pass a value into a child component. The child component will then preform the save operation. The parent doesn't need to know anything about it. I am able to pass in the object but not save its updated form.
Parent
<template>
<div v-show="isOpened">
<EditModal #toggle="closeModal" #update:todo="submitUpdate($event)"
:updatedText="editText" :todo="modalPost" />
</div>
</template>
<script setup lang="ts">
import Post from "../components/Post.vue";
import { api } from "../lib/api";
import { ref } from "vue";
import { onMounted } from "vue-demi";
import EditModal from "../components/EditModal.vue";
const postArr = ref('');
const message = ref('');
let isOpened = ref(false);
let modalPost = ref('');
let editText = ref('');
function closeModal() {
isOpened.value = false
}
function openModal(value: string) {
isOpened.value = true
modalPost.value = value
}
// call posts so the table loads updated item
function submitUpdate(value: any) {
console.log("called update in parent " + value)
editText.value = value
posts()
}
</script>
Child EditModal
<template>
<div>
<div>
<textarea id="updateTextArea" rows="10" :value="props.todo.post"></textarea>
</div>
<!-- Modal footer -->
<div>
<button data-modal-toggle="defaultModal" type="button"
#click="update(props.todo.blogId, props.todo.post)">Save</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { api } from "../lib/api";
import { reactive, ref } from "vue";
const props = defineProps({
todo: String,
updatedText: String,
})
const emit = defineEmits(
['toggle','update:todo']
);
function setIsOpened(value: boolean) {
emit('toggle', value);
}
function update(id: string, value: string) {
console.log('the value ' + value)
try {
api.updateBlog(id, value).then( res => {
emit('update:todo', value)
emit('toggle', false);
})
} catch (e) {
console.log('Error while updating post: '+ e)
}
}
</script>
I know the props are read only, therefore I tried to copy it I can only have one model.
I do not see the reason I should $emit to the parent and pass something to another variable to pass back to the child.
I am trying to pass in text to a modal component where it can edit the text and the child component saves it.
Advice?
First, your component should be named EditTodo no EditModal because you are not editing modal. All Edit components should rewrite props to new local variables like ref or reactive, so you can work on them, they won't be read only any more.
Child EditTodo.vue
<template>
<div>
<div>
<textarea id="updateTextArea" rows="10" v-model="data.todo.title"></textarea>
</div>
<div>
<button data-modal-toggle="defaultModal" type="button" #click="update()">Save</button>
</div>
</div>
</template>
<script setup lang="ts">
import { api } from "../lib/api";
import { reactive } from "vue";
const props = defineProps<{ id: number, todo: { title: string} }>()
const emit = defineEmits(['toggle']);
const data = reactive({ ...props })
// I assuming api.updateBlog will update data in database
// so job here should be done just need toogle false modal
// Your array with todos might be not updated but here is
// no place to do that. Well i dont know how your API works.
// Database i use will automaticly update arrays i updated objects
function update() {
try {
api.updateBlog(data.id, data.todo ).then(res => {
emit('toggle', false);
})
}
}
</script>
Parent
<template>
<div>
<BaseModal v-if="todo" :show="showModal">
<EditTodo :id="todo.id" :todo="todo.todo" #toggle="(value) => showModal = value"></EditTodo>
</BaseModal>
</div>
</template>
<script setup lang="ts">
const showModal = ref(false)
const todo = reactive({ id: 5, todo: { title: "Todo number 5"} })
</script>
I separated a modal object with edit form, so you can create more forms and use same modal. And here is a simple, not fully functional modal.
<template>
<div class="..."><slot></slot></div>
</template>
<script setup>
defineProps(['show'])
defineEmits(['toogle'])
</script>
You might want to close modal when user click somewhere outside of modal.

Using ref with props

I have a Component A with a nested Component B. Component A loads a list of strings, Component B has a SELECT element, which is populated when Component A is finished loading. The first option of the SELECT should be selected at this moment. After this moment, when other options are selected, Component B should emit update event with selection data, so that Component A 'knows' what option was selected. Since A should control B, this option is passed back to B with props.
In my implementation below, only the initial value of the selection is received by Component B, and when the list is loaded, it is populated successfully, but the selectedOption value is not changed by the controlling property. So, it doesn't work.
What would be a proper way to implement this?
Component A:
<template>
<component-b :list="list" :selected="selected" #update="onUpdate">
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup: () => {
const list = ref<string[]>([]);
const selected = ref('');
load().then(data => { // some load function returning a promise
selected.value = data[0]; // data is never empty
list.value = data;
});
const onUpdate = (data: string) => selected.value = data;
return { list, selected, onUpdate };
}
})
</script>
Component B:
<template>
<select v-model="selectedOption" #change="onChange">
<option v-for="item of props.list" />{{item}</option>
</select>
</template>
<script lang="ts">
import { defineComponent, ref, PropType } from 'vue';
export default defineComponent({
emits: ['update'],
props: {
selected: {
type: String
},
list: {
type: Object as PropType<String[]>
}
}
setup: (props, { emit }) => {
const selectedOption = ref(props.selected); // this doesn't work when loading is finished and props are updated
const onChange = () => emit('update', selectedOption.value);
return { selectedOption, onChange, props }
}
});
</script>
Thank you.
In ComponentB.vue, selectedOption's ref is only initialized to the value of props.selected, but that itself is not reactive (i.e., the ref isn't going to track props.selected automatically):
const selectedOption = ref(props.selected); // not reactive to props.selected
You can resolve this with a watchEffect that copies any new values to selectedOption (which happens automatically whenever props.selected changes):
watchEffect(() => selectedOption.value = props.selected);
Side note: setup() doesn't need to explicitly return props because they're already available to the template by name. Your template could be changed to:
<!-- <option v-for="item of props.list">{{ item }}</option> -->
👇 reference props directly by name
<option v-for="item of list">{{ item }}</option>
demo
In component B, you shouldn't need to add both a v-model and a #change.
<select :value="selected" #change="onChange">
<option v-for="item in list">{{ item }}</option>
</select>
Then in the onChange get the value from the select and emit up:
const onChange = (event) => emit('update', event.target.value);

How to destructure object props in Vue the way like you would do in React inside a setup?

I am wondering how to destructure an object prop without having to type data.title, data.keywords, data.image etc. I've tried spreading the object directly, but inside the template it is undefined if I do that.
Would like to return directly {{ title }}, {{ textarea }} etc.
My code:
<template>
<div>
<h1>{{ title }}</h1>
</div>
</template>
<script lang="ts">
import { useSanityFetcher } from "vue-sanity";
import { defineComponent, reactive, toRefs } from "vue";
export default defineComponent({
name: "App",
setup: () => {
const articleQuery = `*[_type == "article"][0] {
title,
textarea,
}`;
const options = {
listen: true,
clientOnly: true,
};
const res = useSanityFetcher<any | object>(articleQuery, options);
const data = reactive(res.data);
return toRefs(data);
},
});
</script>
Considering that useSanityFetcher is asynchronous, and res is reactive, it's incorrect to access res.data directly in setup because this disables the reactivity. Everything should happen in computed, watch, etc callback functions.
title, etc properties need to be explicitly listed in order to map reactive object to separate refs with respective names - can probably be combined with articleQuery definition or instantly available as res.data keys
E.g.:
const dataRefs = Object.fromEntries(['title', ...].map(key => [key, ref(null)]))
const res = ...
watchEffect(() => {
if (!res.data) return;
for (const key in dataRefs)
dataRefs[key] = res.data[key];
});
return { ...dataRefs };
Destructuring the object is not the problem, see Vue SFC Playground
<script lang="ts">
//import { useSanityFetcher } from "vue-sanity";
import { defineComponent, reactive, toRefs } from "vue";
export default defineComponent({
name: "App",
setup: () => {
const res = {
data: {
title: 'Hi there'
}
}
const data = reactive(res.data);
return toRefs(data);
},
});
</script>
<template>
<div>
<h1>{{ title }}</h1>
</div>
</template>
It may simply be the space between the filter and the projection in the GROQ expression
const articleQuery = `*[_type == "article"][0]{ title, textarea }`;
See A description of the GROQ syntax
A typical GROQ query has this form:
*[ <filter> ]{ <projection> }
The Vue docs actually recommend not destructing props because of the way reactivity works but if you really want to something like this should work:
const res = useSanityFetcher<any | object(articleQuery, options);
const data = reactive(res.data);
return toRefs(data);
Don't forget to import reactive and toRefs.

How can I pass a variable value from a "page" to "layout" in Nuxt JS?

I'm a beginner in VUE and donnow this one is the correct syntax. I need the variable {{name}} to be set from a page. Which means I need to change the value of the variable page to page. How can I achieve that? Help me guys.
My "Layout" Code is like below -
<template>
<div class="login-page">
<div class="col1">{{ name }}</div>
<div class="col2">
<div class="content-box">
<nuxt />
</div>
</div>
</div>
</template>
<script>
export default {
props: ['name']
}
</script>
And my "Page" code is following -
<template>
<div>Welcome</div>
</template>
<script>
export default {
layout: 'login',
data: function() {
return {
name: 'Victor'
}
}
}
</script>
this can be achieved by using the vuex module. The layout have access to the vuex store, so once a page is open, you can call a mutation to set the page name and listen the name state in the layout component.
First the Vuex module, we can add a module by creating a file in the store folder,
in this case we are creating the page module:
// page.js file in the store folder
const state = {
name: ''
}
const mutations = {
setName(state, name) {
state.name = name
}
}
const getters = {
getName: (state) => state.name
}
export default {
state,
mutations,
getters
}
Now we can use the setPageName mutation to set the pageName value once a page reach the created hook (also can be the mounted hook):
// Page.vue page
<template>
<div>Welcome</div>
</template>
<script>
export default {
layout: 'login',
created() {
this.$store.commit('page/setName', 'Hello')
},
}
</script>
And in the layout component we have the computed property pageName (or name if we want):
<template>
<div class="login-page">
<div class="col1">{{ name }}</div>
<div class="col2">
<div class="content-box">
<nuxt />
</div>
</div>
</div>
</template>
<script>
export default {
computed: {
name() {
return this.$store.getters['page/getName']
}
}
}
</script>
And it's done!
Answer to your question in the commets:
The idea behind modules is keep the related information to some functionality in one place. I.e Let's say you want to have name, title and subtitle for each page, so the page module state variable will be:
const state = { name: '', title: '', subtitle: ''}
Each variable can be updated with a mutation, declaring:
const mutations = {
setName(state, name) {
state.name = name
},
setPageTitle(state, title) {
state.title = title
},
setPageSubtitle(state, subtitle) {
state.subtitle = subtitle
},
}
And their values can be updated from any page with:
this.$store.commit('page/setPageTitle', 'A page title')
The same if you want to read the value:
computed: {
title() {
// you can get the variable state without a getter
// ['page'] is the module name, nuxt create the module name
// using the file name page.js
return this.$store.state['page'].title
}
}
The getters are good for format or filter information.
A new module can be added anytime if required, the idea behind vuex and the modules is to have a place with the information that is required in many places through the application, in one place. I.e. the application theme information, if the user select the light or dark theme, maybe the colors can be changed. You can read more about vuex with nuxt here: https://nuxtjs.org/guide/vuex-store/ and https://vuex.vuejs.org/

Problem with passing data from Vuex getters to child component

I am creating movie-app with Vue and Vuex which will display all movies in Home component and on click single movie in Details (child) component. I get all movies in Home component but I can not display single movie in Details component.
I have enabled props in child component but I have problem with passing movie ID to method imported from getters (in Details component) which should filter selected movie from movies list.
Home component
<template>
<div class="home">
<router-view />
<h1>Home component:</h1>
<div v-for="movie in movies"
:key="movie.id">
<div>
<router-link :to="'/movie/' + movie.id">{{ movie.title }}</router-link>
</div>
</div>
</div><!--home-->
</template>
<script>
import axios from 'axios'
import { mapGetters, mapActions } from 'vuex'
export default {
name: "home",
methods: {
...mapActions(['fetchMovies']),
},
computed: {
...mapGetters(['movies'])
},
created() {
this.fetchMovies()
}
};
</script>
Details component
<template>
<div>
<h1>Movie details - child component</h1>
<h2>Title: {{ movie.title }}</h2>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
props: ['id'],
computed: {
movie() {
return this.$store.getters.singleMovie(this.id)
}
}
}
</script>
Vuex store
const state = {
movies: []
};
const getters = {
movies: state => state.movies,
singleMovie: state => movieId => {
return state.movies.find(item => item.id === movieId);
}
};
const actions = {
async fetchMovies({ commit }) {
const response = await axios.get(
movie_url + "movie/popular" + "?api_key=" + api_key
);
commit("setMovies", response.data.results);
}
};
const mutations = {
setMovies: (state, items) => (state.movies = items)
};
export default {
state,
getters,
actions,
mutations
};
I tried to replace {{movie.title}} with {{id}} and then I get displayed movie ID as I click on listed movies in Home component. I also tried to hard code movie id as parameter: return this.$store.getters.singleMovie(299536) and I successfully get title displayed but in console I get error "TypeError: Cannot read property 'title' of undefined". Obviously I am making mistake with passing delivered movie ID.
Your computed property movie is not loaded in every vue life cycle. So, in the first time you get movie, it is undefined and your mustache {{ movie.title }} is actually something like {{ 'undefined'.title }}, which causes the error.
To solve it, you can both add a v-if="movie" conditional in template or return a default value in computed property movie. Like this, for example: return state.movies.find(item => item.id === movieId) || {};