Compiled Runtime Ordering of Watchers in Vue 3 - vue.js

So, let's hypothetically say I have the following setup:
<script setup lang="ts">
const someRef = ref<null | string>(null)
const setSomeRef = (v: string) => {
someRef.value = v
}
const watcher1 = watch(someRef, () => {
doSomething()
})
const watcher2 = watch(someRef, () => {
doSomethingElse()
})
</script>
<template>
<button #click="setSomeRef("Hello!")">Hit Me!</button>
</template>
Let's also just hypothetically imagine that these two watchers might be in two different composables, but maybe instantiated on the same component ... for reasons.
A user clicks the button.
My thought is, which function fires first? doSomething() or `doSomethingElse()?
Do they always fire in the same order? ... and does it matter if the side effects are unrelated, i.e., is the perceived difference negligible?

Seems like this should not be a concern in JavaScript. And you are proberbly doing something wrong, if you need this. You need more control and predictability than this.
I did a very small test, that might help you.
Seems like the code runs synchronous (as expected in JavaScript) with less than .3 milliseconds in between (on my machine).
Test in Playground
<script setup lang="ts">
import {ref, watch} from 'vue'
const test = ref([])
const someRef = ref(0)
const setSomeRef = () => {
someRef.value++
}
const watcher1 = watch(someRef, () => {
test.value.push("1: " + window.performance.now())
console.log("1: ", window.performance.now())
})
const watcher2 = watch(someRef, () => {
test.value.push("2: " + window.performance.now())
console.log("2: ", window.performance.now())
})
</script>
<template>
<button #click="setSomeRef()">Hit Me!</button>
<div v-for="i in test">
{{i}}
</div>
</template>

Related

Is there a 'simple' way to dynamically render views in vue?

Let's take a step back and look at the use case:
You're defining a modular interface, and any module that implements it must be able to 'render itself' into the application given a slot and a state.
How do you do it in vue?
Example solution
Let's have a look at the most basic implementation I can assemble:
(full example:
https://stackblitz.com/edit/vitejs-vite-8zclnp?file=src/App.vue)
We have a layout:
# Layout.vue
<template>
<div>
<hr />
<slot name="moduleView" />
<hr />
</div>
</template>
...and an app with a module:
# App.vue
<script lang="ts" setup>
import type { MyModuleState } from "./MyModule";
import Layout from "./Layout.vue";
import { ref } from "vue";
import { MyModule } from "./MyModule";
import ModView from "./ModView.vue";
const state = ref<MyModuleState>({ value: 0 });
const module = new MyModule();
const onClick = () => {
state.value = { value: state.value.value + 1 };
};
const renderModule = () => {
return module.view(state.value);
};
</script>
<template>
<div>currentValue: {{ state.value }}</div>
<div>update: <button #click="onClick">++</button></div>
<div>
<Layout>
<template v-slot:moduleView>
<mod-view :render="renderModule" :state="state" /> // <--- But this!
</template>
</Layout>
</div>
</template>
...but rendering into the slot requires a lot of jumping through obscure hoops:
# ModView.vue
<script lang="ts" setup>
import ModRender from "./ModRender";
import { ref, watch } from "vue";
import type { VNode } from "vue";
const props = defineProps<{
render: (state?: any) => VNode | Array<VNode>;
state?: any;
}>();
const nodes = ref(props.render(props.state));
watch( // <-- Obscure! The view won't update unless you explicitly watch props?
() => props.state,
(nextState) => {
nodes.value = props.render(nextState);
}
);
</script>
<template>
<mod-render :nodes="nodes" />
</template>
# ModRender.ts
import type { VNode } from "vue";
const ModRender = (props: { nodes: VNode | Array<VNode> }) => {
return props.nodes;
};
ModRender.props = {
nodes: {
required: true,
},
};
export default ModRender; // <--- Super obscure, why do you need a functional component for this?
Before we can define the actual module:
# MyModule.ts
import type { VNode } from "vue";
import { h } from "vue";
import ModuleView from "./MyModuleDisplay.vue";
interface AbstractModule<T> {
view: (state: T) => VNode;
}
export interface MyModuleState {
value: number;
}
export class MyModule implements AbstractModule<MyModuleState> {
view(state: MyModuleState): VNode {
return h(ModuleView, { state });
}
}
...and a component for it:
# MyModuleView.vue
<script setup lang="ts">
import type { MyModuleState } from "./MyModule";
const props = defineProps<{ state: MyModuleState }>();
</script>
<template>
<div>{{ state.value }}</div>
</template>
What.
This seems extremely obtuse and verbose.
Am I missing something?
In other component systems an implementation might look like:
export class MyModule implements AbstractModule<MyModuleState> {
view(state: MyModuleState): VNode {
return (<div>{state.value}</div>);
}
}
...
<div>
<Layout>{renderModule(state)}</Layout>
</div>
It seems surprising that so many wrappers and hoops have to be done in vue to do this, which makes me feel like I'm missing something.
Is there an easier way of doing this?
Vnode objects cannot be rendered in component templates as is and need to be wrapped in a component like ModRender. If they are used as universal way to exchange template data in the app, that's a problem. Vnodes still can be directly used in component render functions and functional components with JSX or h like <Layout>{renderModule(state)}</Layout>, this limits their usage.
AbstractModule convention may need to be reconsidered if it results in unnecessary code. Proceed from the fact that a "view" needs to be used with dynamic <component> at some point, and it will be as straightforward as possible.
There may be no necessity for "module" abstraction, but even if there is, module.view can return a component (functional or stateful) instead of vnodes. Or it can construct a component and make it available as a property, e.g.:
class MyModule {
constructor(state) {
this.viewComponent = (props) => h(ModuleView, { state, ...props })
}
}

Nuxt3: how two chain two fetches?

I am trying to chain two fetch in Nuxt3, with the second one calling an URL based on the result of the first one and the resulting "variable" to be used in the Vue component.
I'm trying with
<script setup>
const url = "myurl";
const { data } = await useFetch(url);
watch(data, async () => {
// do something with the result
url2 = data.value.result
const variable = await useFetch(url2);
});
</script>
but it looks like the block inside the watch is not able to modify the values of the variables at all (e.g. if I define it outside and try to update it inside, even with hard coded values)
Am I missing something very obvious here?
Something like this works perfectly fine, since all the fetching will be awaited
<script setup>
const { data } = await useFetch('https://jsonplaceholder.typicode.com/todos/1')
console.log('data', data.value.userId)
const { data: photos } = await useFetch(`https://jsonplaceholder.typicode.com/photos/${data.value.userId}`)
console.log('data2', photos.value)
</script>
<template>
<div>
first data: {{ data }}
</div>
<hr />
<div>
photos: {{ photos }}
</div>
</template>

How to test "errorComponent" in "defineAsyncComponent" in Vue?

I was learning about Async Components in Vue. Unfortunately in that documentation Vue did not show any example of using Async Components in the <template> part of a Vue SFC. So after searching on the web and reading some articles like this one and also this one, I tried to use this code to my Vue component:
<!-- AsyncCompo.vue -->
<template>
<h1>this is async component</h1>
<button #click="show = true">login show</button>
<div v-if="show">
<LoginPopup></LoginPopup>
</div>
</template>
<script>
import { defineAsyncComponent, ref } from 'vue';
import ErrorCompo from "#/components/ErrorCompo.vue";
const LoginPopup = defineAsyncComponent({
loader: () => import('#/components/LoginPopup.vue'),
/* -------------------------- */
/* the part for error handling */
/* -------------------------- */
errorComponent: ErrorCompo,
timeout: 10
}
)
export default {
components: {
LoginPopup,
},
setup() {
const show = ref(false);
return {
show,
}
}, // end of setup
}
</script>
And here is the code of my Error component:
<!-- ErrorCompo.vue -->
<template>
<h5>error component</h5>
</template>
Also here is the code of my Route that uses this component:
<!-- test.vue -->
<template>
<h1>this is test view</h1>
<AsyncCompo></AsyncCompo>
</template>
<script>
import AsyncCompo from '../components/AsyncCompo.vue'
export default {
components: {
AsyncCompo
}
}
</script>
And finally the code of my actual Async component called LoginPopup.vue that must be rendered after clicking the button:
<!-- LoginPopup.vue -->
<template>
<div v-if="show1">
<h2>this is LoginPopup component</h2>
<p>{{retArticle}}</p>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
setup() {
const getArticleInfo = async () => {
// wait 3 seconds to mimic API call
await new Promise(resolve => setTimeout(resolve, 3000));
const article = "my article"
return article
}
const show1 = ref(false);
const retArticle = ref(null);
onMounted(
async () => {
retArticle.value = await getArticleInfo();
show1.value = true;
}
);
return {
retArticle,
show1
}
}
}
</script>
When I comment the part below from AsyncCompo.vue everything works correctly and my component loads after 3s when I clicks the button:
errorComponent: ErrorCompo,
timeout: 10
But I want to test the error situation that Vue says in my component. I am not sure that my code implementation is absolutely true, but with code above when I use the errorComponent, I receive this warning and error in my console:
I also know that we could handle these situations with <Suspense> component, but because my goal is learning Async Components, I don't want to use them here. Could anyone please help me that how I can see and test my "error component" in the page? is my code wrong or I must do something intentionally to make an error? I don't know but some articles said that with decreasing timeout option I could see error component, but for me it gives that error.

How to use vue 3 suspense component with a composable correctly?

I am using Vue-3 and Vite in my project. in one of my view pages called Articles.vue I used suspense component to show loading message until the data was prepared. Here is the code of Articles.vue:
<template>
<div class="container">
<div class="row">
<div>
Articles menu here
</div>
</div>
<!-- showing articles preview -->
<div id="parentCard" class="row">
<div v-if="error">
{{ error }}
</div>
<div v-else>
<suspense>
<template #default>
<section v-for="item in articleArr" :key="item.id" class="col-md-4">
<ArticlePrev :articleInfo = "item"></ArticlePrev>
</section>
</template>
<template #fallback>
<div>Loading...</div>
</template>
</suspense>
</div>
</div> <!-- end of .row div -->
</div>
</template>
<script>
import DataRelated from '../composables/DataRelated.js'
import ArticlePrev from "../components/ArticlePrev.vue";
import { onErrorCaptured, ref } from "vue";
/* start export part */
export default {
components: {
ArticlePrev
},
setup (props) {
const error = ref(null);
onErrorCaptured(e => {
error.value = e
});
const {
articleArr
} = DataRelated("src/assets/jsonData/articlesInfo.json");
return {
articleArr,
error
}
}
} // end of export
</script>
<style scoped src="../assets/css/viewStyles/article.css"></style>
As you could see I used a composable js file called DataRelated.js in my page that is responsible for getting data (here from a json file). This is the code of that composable:
/* this is a javascript file that we could use in any vue component with the help of vue composition API */
import { ref } from 'vue'
export default function wholeFunc(urlData) {
const articleArr = ref([]);
const address = urlData;
const getData = async (address) => {
const resp = await fetch(frontHost + address);
const data = await resp.json();
articleArr.value = data;
}
setTimeout(() => {
getData(address);
}, 2000);
return {
articleArr
}
} // end of export default
Because I am working on local-host, I used JavaScript setTimeout() method to delay the request to see that the loading message is shown or not. But unfortunately I think that the suspense component does not understand the logic of my code, because the data is shown after 2000ms and no message is shown until that time. Could anyone please help me that what is wrong in my code that does not work with suspense component?
It's a good practice to expose a promise so it could be chained. It's essential here, otherwise you'd need to re-create a promise by watching on articleArr state.
Don't use setTimeout outside the promise, if you need to make it longer, delay the promise itself.
It could be:
const getData = async (address) => {
await new Promise(resolve => setTimeout(resolve, 2000);
const resp = await fetch(frontHost + address);
const data = await resp.json();
articleArr.value = data;
}
const promise = getData(urlData)
return {
articleArr,
promise
}
Then:
async setup (props) {
...
const { articleArr, promise } = DataRelated(...);
await promise
...
If DataRelated is supposed to be used exclusively with suspense like that, it won't benefit from being a composable, a more straightforward way would be is to expose getData instead and make it return a promise of the result.

Making API call using Axios with the value from input, Vue.js 3

I am making an app using this API. The point I'm stuck with is calling the API. If I give the name of the country, the data of that country comes.
Like, res.data.Turkey.All
I want to get the value with input and bring the data of the country whose name is entered.
I am getting value with searchedCountry. But I can't use this value. My API call does not happen with the value I get. I'm getting Undefined feedback from Console.
Is there a way to make a call with the data received from the input?
<template>
<div>
<input
type="search"
v-model="searchedCountry"
placeholder="Search country"
/>
</div>
</template>
<script>
import axios from 'axios';
import { ref, onMounted} from 'vue';
export default {
setup() {
let data = ref([]);
const search = ref();
let searchedCountry = ref('');
onMounted(() => {
axios.get('https://covid-api.mmediagroup.fr/v1/cases').then((res) => {
data.value = res.data.Turkey.All;
});
});
return {
data,
search,
searchedCountry,
};
},
};
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
I'm work with Vue.js 3
There are a few things wrong with your code:
Your axios call is only called once, when the component mounts (side note here, if you really want to do something like that, you can do it directly within the setup method)
You don't pass the value from searchedCountry to the axios API
Use const for refs
I'd use a watch on the searchedCountry; something like this (I don't know the API contract):
<template>
<div>
<input
type="search"
v-model="searchedCountry"
placeholder="Search country"
/>
</div>
</template>
<script>
import axios from 'axios';
import { ref, watch } from 'vue';
export default {
setup() {
const searchedCountry = ref('');
const data = ref([]);
watch(
() => searchedCountry,
(country) => axios.get(`https://covid-api.mmediagroup.fr/v1/cases/${country}`).then((res) => data.value = res.data.Turkey.All);
);
return {
data,
searchedCountry,
};
},
};
</script>