How to make provide/inject reactive in Vue 3 to avoid props drilling? - vue.js

I have a root component that has a lot of descendants. In order to avoid props drilling, I want to use provide/inject.
In the root component in the setup function, I use provide.
In the child component in the setup function, I get the value via inject.
Then the child component might emit an event, that forces the root component to reload data that it provides to the child components.
However, the data in the child component is not changed.
Previous answers that I found usually were related to Vue 2, and I'm struggling with Vue 3 composition API.
I tried to use watch/watchEffect, and "re-provide" the data, but it didn't work (and I'm not sure if it's a good solution).
Sample code: https://codesandbox.io/s/mystifying-diffie-e3eqyq

I don't want to be that guy, but read the docs!
Anyway:
App.vue
setup() {
let randomNumber = ref(Math.random());
function updateRandomNumber() {
randomNumber.value = Math.random()
}
// This should be an AJAX call to re-configurate all the children
// components. All of them needs some kind of config.
// How to "re-provide" the data after a child component asked this?
provide("randomNumber", {
randomNumber,
updateRandomNumber
});
},
ChildComponent.vue
<template>
<div>Child component</div>
<button #click="updateRandomNumber">Ask root component for re-init</button>
<div>Injected data: {{ randomNumber }}</div>
</template>
<script>
import { inject } from "vue";
export default {
setup() {
// How to "re-inject" the data from parent?
const {randomNumber, updateRandomNumber} = inject("randomNumber");
return {
randomNumber,
updateRandomNumber
};
},
};
</script>

Related

Find nearest parent Vue component of template ref (Vue 3)

When a Vue template ref is mounted, I want to get the nearest parent Vue component. This should be generic and work for any template ref so I've put it in a composition function (but that's just an implementation detail).
I had this working but my implementation used elem.__vueParentComponent while iteratively searching an element's ancestors. While reading the Vue source code I saw __vueParentComponent was only enabled for dev mode or if dev tools is enabled in production. Thus, I don't want to rely on that flag being enabled.
I thought this might be possible using vnodes but this isn't easily google-able. Here's an example of what I'm trying to do:
function useNearestParentInstance(templateRef) {
function getNearestParentInstance(el) {
// code here
}
onMounted(() => {
const el = templateRef.value;
const instance = getNearestParentInstance(el);
// do something with instance
});
}
<template>
<div>
<SomeComponent>
<div>
<div ref="myElem"></div>
</div>
</SomeComponent>
</div>
</template>
<script>
export default {
setup() {
const myElem = ref();
// nearest would be SomeComponent's instance in this case
useNearestParentInstance(myElem);
...
}
}
</script>
If you want the nearest vue parent you can simply use
ref().$parent // Not sure if syntax is same in vue3
ref().$parent will get the first vuecomponent that is the parent of the ref that you placed.

Why vue need to forceUpdate components when they include static slot

Why vue needs to forceUpdate child component that has a static slot when it update self
It will trigger too much update calculate when a component has lots of child components that has a static slot
// my-button.vue
<template>
<div>
<slot></slot>
</div>
</template>
// my-com.vue
<template>
<div>
<span>{{ foo }}</span>
<template v-for="(item, index) in arr">
<my-button>test</my-button>
</template>
</div>
</template>
<script>
export default {
data() {
return {
foo: 1,
arr: (new Array(10000)).fill(1)
}
}
}
</scirpt>
If run this.foo = 2 will lead update queue include 10000 watcher. When I read source code I found the following code
function updateChildComponent (
...
// Any static slot children from the parent may have changed during parent's
// update. Dynamic scoped slots may also have changed. In such cases, a forced
// update is necessary to ensure correctness.
const needsForceUpdate = !!(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
hasDynamicScopedSlot
)
...
// resolve slots + force update if has children
if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
...
}
I have found this issue on GitHub.
Unfortunately, any child components with static slot content still
need to be forced updated. This means the common use case of
<parent><child></child></parent> doesn't benefit from this change,
unless the default slot is explicitly forced into a scoped slot by
using <parent v-slot:default><child></child></parent>. (We cannot
directly force all slots into scoped slots as that would break
existing render function code that expects the slot to be present on
this.$slots instead of this.$scopedSlots)
Seems like it's fixed in 2.6.
In 2.6, we have introduced an optimization that further ensures parent
scope dependency mutations only affect the parent and would no longer
force the child component to update if it uses only scoped slots.
To solve your problem just update your Vue version to 2.6. Since it's just a minor update nothing will break down. What about the reason to call forceUpdate - only Evan You knows that :)
Hello i solved this problem with this:
export function proxySlots(scopedSlots: any): any {
return Object.keys(scopedSlots).reduce<any>(
(acc, key) => {
const fn = scopedSlots[key];
fn.proxy = true;
return { ...acc, [key]: fn };
},
{ $stable: true },
);
}
const ctx = {
// ...
scopedSlots: proxySlots({ someSlot: () => <span>Hello</span>})
// or from provided slots : this.$scopedSlots or context.slots if using composition api
}
It's a bit hackish but no more forced update.

How do I update a property in parent component from a child component

I have a parent component set up with data property(boolean) like this:
<script>
import PmhnEntryAll from './components/PmhnEntryAll'
import {store} from './store.js'
export default {
name: 'PmhnApp',
components: {
PmhnEntryAll,
},
data: function () {
return {
vPmhnEntryAll: store.componentVisibility.vPmhnEntryAll,
}
},
}
</script>
and in child component I want to set the vPmhnEntryAll to 'true'. How do I go about this ?
this is usually done using v-on: in parent and $emit in child
check out https://v2.vuejs.org/v2/guide/components-custom-events.html for docs around events emitting
you can also go further and v-bind which is a form of event listening, but it's limited in what applications you can use it for. https://v2.vuejs.org/v2/guide/components-custom-events.html#Binding-Native-Events-to-Components
You can also pass a function to update a variable, but this is considered an anti-pattern and not recommended.
And depending on the structure of your project, you can uses busses or vuex for managing global scope.

Vue 2 pass props to child [old : "call child's method"]

ok so I've learned that I'm not supposed to call a child's method but pass it props instead.
I've got (parent) :
<template>
<div id="main">
<Header :title ="title"/>
<router-view/>
<LateralMenu/>
</div>
</template>
<script>
export default {
name: 'app'
data: function () {
return {
title: true
}
},
methods: {
hideTitle: function () {
this.title = false
console.log(this.title)
},
showTitle: function () {
this.title = true
console.log(this.title)
}
}
}
</script>
and (child) :
<script>
export default {
name: 'Header',
props: ['title'],
created () {
console.log(this.title)
},
methods: {
}
}
</script>
the first console logs (inside the parent) print correctly on each method but the second console log within the child stays true all the time. I got this from : Pass data from parent to child component in vue.js
inside what method does the console.log need to be to be printed everytime the methods in the parent are triggered?
(this is why I wanted to go for method-calling, originally, by going with variables instead, we're potentially omitting valuable parts of the process such as optimization and a "when" for the execution(s!!) of our code. pontetally being the key word here, don't blow up on me, keep in mind that I'm learning.)
OLD:
I've browsed the web and I know there a a million different answers
and my point is with the latest version of vue none of those millions
of answers work.
either everything is deprecated or it just doesn't apply but I need a
solution.
How do you call a child method?
I have a 1 component = 1 file setup.
DOM is declared inside a <template> tag javascript is written inside
a <script> tag. I'm going off of vue-cli scaffolding.
latest method I've tried is #emit (sometimes paired with an #on
sometimes not) doesn't work :
child :
<script>
export default {
name: 'Header',
created () {
this.$on('hideTitlefinal', this.hideTitlefinal)
},
methods: {
hideTitlefinal: function () {
console.log('hideeeee')
},
showTitlefinal: function () {
console.log('shwowwww')
}
}
}
</script>
parent :
<template>
<div id="main">
<Header v-on:hideTitle="hideTitlefinal" v-on:showTitle="showTitlefinal"/>
<router-view/>
<LateralMenu/>
</div>
</template>
<script>
export default {
methods: {
hideTitle: function () {
this.$emit('hideTitle')
},
showTitle: function () {
this.$emit('showTitle')
}
}
}
</script>
console :
Uncaught TypeError: this.$emit is not a function
at Object.showTitle (Main.vue?1785:74)
at VueComponent.showTitle (LateralMenu.vue?c2ae:113)
at boundFn (vue.esm.js?efeb:186)
at invoker (vue.esm.js?efeb:1943)
at HTMLDivElement.fn._withTask.fn._withTask (vue.esm.js?efeb:1778)
Please don't do this. You're thinking in terms of events. When x happens, do y. That's sooo jquery 2005 man. Vue still has all that stuff, but we're being invited to think in terms of a view model...
You want your state in a variable, in window scope, and you want reactive pipes linking your vue stuff to your state object. To toggle visibility, use a dynamic class binding, or v-if. Then think about how to represent your state. It could be as simple as having a property like store.titleVisible. But, you want to 'normalize' your store, and avoid relationships between items of state. So if title visibility really depends on something higher up, like an editMode or something, then just put the higher-up thing in the store, then create computed properties if you need them.
The goal is that you don't care when things happen. You just define the relationships between the markup and the store, then let Vue take care of it. The docs will tell you to use props for parent=>child and $emit for child=>parent communication. Truth is you don't need this until you have multiple instances of a component, or reusable components. Vue stuff talks to a store, not to other vue stuff. For single-use components, as for your root Vue, just use the data:.
Whenever you find yourself writing show/hide methods, you're doing it wrong. It's intuitive (because it's procedural), but you'll quickly appreciate how much better the MVVM approach is.

VueJS access child component's data from parent

I'm using the vue-cli scaffold for webpack
My Vue component structure/heirarchy currently looks like the following:
App
PDF Template
Background
Dynamic Template Image
Static Template Image
Markdown
At the app level, I want a vuejs component method that can aggregate all of the child component's data into a single JSON object that can be sent off to the server.
Is there a way to access child component's data? Specifically, multiple layers deep?
If not, what is the best practice for passing down oberservable data/parameters, so that when it's modified by child components I have access to the new values? I'm trying to avoid hard dependencies between components, so as of right now, the only thing passed using component attributes are initialization values.
UPDATE:
Solid answers. Resources I found helpful after reviewing both answers:
Vuex and when to use it
Vuex alternative solution for smaller apps
In my child component, there are no buttons to emit changed data. It's a form with somewhat 5~10 inputs. the data will be submitted once you click the process button in another component. so, I can't emit every property when it's changing.
So, what I did,
In my parent component, I can access child's data from "ref"
e.g
<markdown ref="markdowndetails"></markdown>
<app-button #submit="process"></app-button>
// js
methods:{
process: function(){
// items is defined object inside data()
var markdowns = this.$refs.markdowndetails.items
}
}
Note: If you do this all over the application I suggest move to vuex instead.
For this kind of structure It's good to have some kind of Store.
VueJS provide solution for that, and It's called Vuex.If you are not ready to go with Vuex, you can create your own simple store.
Let's try with this
MarkdownStore.js
export default {
data: {
items: []
},
// Methods that you need, for e.g fetching data from server etc.
fetchData() {
// fetch logic
}
}
And now you can use those data everywhere, with importing this Store file
HomeView.vue
import MarkdownStore from '../stores/MarkdownStore'
export default {
data() {
sharedItems: MarkdownStore.data
},
created() {
MarkdownStore.fetchData()
}
}
So that's the basic flow that you could use, If you dont' want to go with Vuex.
what is the best practice for passing down oberservable data/parameters, so that when it's modified by child components I have access to the new values?
The flow of props is one way down, a child should never modify its props directly.
For a complex application, vuex is the solution, but for a simple case vuex is an overkill. Just like what #Belmin said, you can even use a plain JavaScript object for that, thanks to the reactivity system.
Another solution is using events. Vue has already implemented the EventEmitter interface, a child can use this.$emit('eventName', data) to communicate with its parent.
The parent will listen on the event like this: (#update is the shorthand of v-on:update)
<child :value="value" #update="onChildUpdate" />
and update the data in the event handler:
methods: {
onChildUpdate (newValue) {
this.value = newValue
}
}
Here is a simple example of custom events in Vue:
http://codepen.io/CodinCat/pen/ZBELjm?editors=1010
This is just parent-child communication, if a component needs to talk to its siblings, then you will need a global event bus, in Vue.js, you can just use an empty Vue instance:
const bus = new Vue()
// In component A
bus.$on('somethingUpdated', data => { ... })
// In component B
bus.$emit('somethingUpdated', newData)
you can meke ref to child component and use it as this
this.$refs.refComponentName.$data
parent-component
<template>
<section>
<childComponent ref="nameOfRef" />
</section>
</template>
methods: {
save() {
let Data = this.$refs.nameOfRef.$data;
}
},
In my case I have a registration form that I've broken down into components.
As suggested above I used $refs, In my parent I have for example:
In Template:
<Personal ref="personal" />
Script - Parent Component
export default {
components: {
Personal,
Employment
},
data() {
return {
personal: null,
education: null
}
},
mounted: function(){
this.personal = this.$refs.personal.model
this.education = this.$refs.education.model
}
}
This works well as the data is reactive.