Nuxt3 useFetch dynamic request not working - nuxt3

I want to send multiple requests with different url parameters like a pagination feature. I found that useAsyncData works well but useFetch doesn't work. Is this a bug in nuxt codebase or not?
the codesandbox is here: https://codesandbox.io/p/sandbox/little-fog-y5gevk?file=%2Fapp.vue&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A33%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A1%7D%5D
<script setup>
const count = ref(1);
// doesn't work
// const { data, refresh } = await useFetch(`/api/hello`, {
// query: { count: count.value },
// });
// it works
const { data, refresh } = await useAsyncData("hello", () =>
$fetch("/api/hello", {
query: { count: count.value },
})
);
function add() {
count.value++;
refresh();
}
</script>
<template>
<NuxtExampleLayout example="composables/use-fetch">
<div>
Fetch result:
<pre class="text-left"><code>{{ data }}</code></pre>
<NButton #click="count++"> + </NButton>
<NButton #click="add">add</NButton>
</div>
</NuxtExampleLayout>
</template>

Related

Dynamic routing using graphQL in a Next.js app

I'm building a webpage that consumes the spaceX graphQL api, using apollo as a client. On the landing page I want to display a 'launches' card, that when clicked on, directs to a new page with details about that particular launch, as below:
index.js
import { ApolloClient, InMemoryCache, gql } from "#apollo/client"
import Link from 'next/link'
export const getStaticProps = async () => {
const client = new ApolloClient({
uri: 'https://api.spacex.land/graphql/',
cache: new InMemoryCache()
})
const { data } = await client.query({
query: gql`
query GetLaunches {
launchesPast(limit: 10) {
id
mission_name
launch_date_local
launch_site {
site_name_long
}
links {
article_link
video_link
mission_patch
}
rocket {
rocket_name
}
}
}
`
});
return {
props: {
launches: data.launchesPast
}
}
}
export default function Home({ launches }) {
return (
<div>
{launches.map(launch => {
return(
<Link href = {`/items/${launch.id}`} key = {launch.id}>
<a>
<p>{launch.mission_name}</p>
</a>
</Link>
)
})}
</div>
)
}
I've set up a new page items/[id].js to display information about individual launches, but this is where the confusion is. Using a standard REST api I'd simply use fetch, then append the id to the end of the url to retrieve the desired data. However I'm not sure how to do the equivalent in graphQL, using the getStaticPaths function. Any suggestions?
Here's items/[id]/js, where I'm trying to render the individual launch data:
import { ApolloClient, InMemoryCache, gql } from "#apollo/client"
export const getStaticPaths = async () => {
const client = new ApolloClient({
uri: "https://api.spacex.land/graphql/",
cache: new InMemoryCache(),
});
const { data } = await client.query({
query: gql`
query GetLaunches {
launchesPast(limit: 10) {
id
}
}
`,
});
const paths = data.map((launch) => {
return {
params: { id: launch.id.toString() },
};
});
return {
paths,
fallback:false
}
};
export const getStaticProps = async (context) => {
const id = context.params.id
// not sure what to do here
}
const Items = () => {
return (
<div>
this is items
</div>
);
}
export default Items;
for getStaticPaths
export const getStaticPaths = async () => {
const { data } = await client.query({
query: launchesPastQuery, // this will query the id only
});
return {
paths: data.CHANGE_THIS.map((param) => ({
params: { id: param.id },
})),
fallback: false,
};
};
CHANGE_THIS is the Query Type that follows data in the JSON response.
for getStaticProps
export const getStaticProps = async ({
params,
}) => {
const { data } = await client.query({
query: GetLaunchPastByID ,
variables: { LaunchID: params.id, idType: "UUID" }, // the idType is optional, and the LaunchID is what you'll use for querying by it*
});
return {
props: {
launchesPast: data.CHANGE_THIS,
},
};
The launchPastQueryByID is like:
const GetLaunchPastByID = gql`
query LaunchPastByID($LaunchID: UUID!) { // UUID is id type
CHANGE_THIS(id: $LaunchID) {
id
//...
}
}
`;
sorry for not giving you the correct queries, spacex.land is currently down.

Can't use React useEffect and also build failed using Gatsby

I am building a headless eCommerce website using Nacelle, Gatsby, and Shopify plus.
My problem is that I integrated Okendo API to fetch product reviews and can't build the project.
Actually, as you know, headless eCommerce is a new technology to us, but it is mostly close to Gatsby and SSR.
I tried to go 2 ways, one is to include the script to head using gatsby-react-helmet, and another one is to call window api inside useEffect or useLayoutEffect.
1. Including the script to head tag using gatsby-plugin-react-helmet.
ProductReview.js
import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet';
import transformProductId from '../../utils/transformProductId';
import { PRODUCT_REVIEW_METAFIELD_KEY, OKENDO_SUBSCRIBER_ID } from '../../constants';
const ProductReview = ({
product
}) => {
const OkendoSettings = {
filtersEnabled: true,
omitMicrodata: true,
subscriberId: OKENDO_SUBSCRIBER_ID,
widgetTemplateId: "default"
}
return (
<>
<Helmet>
<script type="application/javascript" src="../plugins/okendo/index.js" />
<script type="application/json" id="oke-reviews-settings">
{JSON.stringify(OkendoSettings)}
</script>
<script type="application/javascript" src="../plugins/okendo/initAPI.js" />
</Helmet>
<div
data-oke-reviews-widget
data-oke-reviews-product-id={transformProductId(product.id)}
/>
</>
);
};
export default React.memo(ProductReview);
/plugin/okendo/index.js
(function () {
function asyncLoad() {
var urls = ['https:\/\/d3hw6dc1ow8pp2.cloudfront.net\/reviewsWidget.min.js?shop=example.myshopify.com'];
for (var i = 0; i < urls.length; i++) {
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = urls[i];
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
}
}
if (window.attachEvent) {
window.attachEvent('onload', asyncLoad);
} else {
window.addEventListener('load', asyncLoad, false);
}
})();
/plugin/okendo/initAPI.js
window.okeReviewsWidgetOnInit = function (okeInitApi) {};
If I include the Okendo scripts to head tag, it works all fine.
But when I try to build on vercel, it says "error Building static HTML failed for path /products/example-product-slug".
2. Calling window.init api inside useEffect.
ProductReview.js
import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet';
import transformProductId from '../../utils/transformProductId';
import { PRODUCT_REVIEW_METAFIELD_KEY, OKENDO_SUBSCRIBER_ID } from '../../constants';
const ProductReview = ({
product
}) => {
const OkendoSettings = {
filtersEnabled: true,
omitMicrodata: true,
subscriberId: OKENDO_SUBSCRIBER_ID,
widgetTemplateId: "default"
}
useEffect(() => {
if (typeof window !== `undefined` && window.okendoInitApi) {
const reviewsWidget = window.document.querySelector('#oke-reviews-widget');
window.okendoInitApi.initReviewsWidget(reviewsWidget);
}
}, [product.id]);
return (
<>
<Helmet>
<script type="application/javascript" src="../plugins/okendo/index.js" />
<script type="application/json" id="oke-reviews-settings">
{JSON.stringify(OkendoSettings)}
</script>
{/* <script type="application/javascript" src="../plugins/okendo/initAPI.js" /> */}
</Helmet>
<div
id="oke-reviews-widget"
data-oke-reviews-widget
data-oke-reviews-product-id={transformProductId(product.id)}
/>
</>
);
};
export default React.memo(ProductReview);
While I am using useEffect to initialize Okendo api, it works only when the page refresh, not work if I open a page.
And if I try to build it, it says "error "window" is not available during server side rendering.".
I know useEffect doesn’t run unless it’s in the browser, but still I don't get what the solution is.
Hope to hear a good news.
Thank you.
UPDATE: The product id is generated from Shopify product graphql data named handle.
gatsby-node.js
exports.createPages = async ({ graphql, actions: { createPage } }) => {
// Fetch all products
const products = await graphql(`
{
allNacelleProduct (filter: { availableForSale: {eq: true} }) {
edges {
node {
handle
}
}
}
}
`);
products.data.allNacelleProduct.edges.forEach((product) =>
createPage({
// Build a Product Detail Page (PDP) for each product
path: `/products/${product.node.handle}`,
component: path.resolve('./src/templates/product-detail.js'),
context: {
handle: product.node.handle
}
})
);
...

Why does it does not return any data when using Async with Axios in Nuxt?

I dont get any of the data in my list. When I use the fetch it works (please see the comment code in the script tag), but not when I use the axios.
Here is the code:
<template>
<div>
<ul>
<li v-for="(mountain, index) in mountains" :key="index">
{{ mountain.title }}
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
mountains: [],
};
},
/*async fetch() {
this.mountains = await fetch(
"https://api.nuxtjs.dev/mountains"
).then((res) => res.json());
}, */
async asyncData() {
const mountains = await axios.get(`https://api.nuxtjs.dev/mountains`);
return { mountains };
},
};
</script>
JSON from https://api.nuxtjs.dev/mountains
Try not to destructure the response
async asyncData() {
const mountains = await axios.get(`https://api.nuxtjs.dev/mountains`);
return { mountains };
},
In the help documents of Nuxt Axios I found out you need to add $axios as a parameter and add a $ before axios and get. See code below:
async asyncData({ $axios }) {
const mountains = await $axios.$get(`https://api.nuxtjs.dev/mountains`);
return { mountains };
},
Now it works perfect!

Error in mounted hook: "ReferenceError: posts is not defined in Vuejs

I'm newbie in Vuejs. I am try to learning to code vuejs for couple of hours until I get this error.I guess the problem is come from props to to Blade. Here is my code.
// Blade View
<div id="app">
<div :posts="{{ $posts }}"></div>
</div>
// Vue Template
<table striped hover :items="imageList">
<template slot="image" slot-scope="data">
<img :src="'storage/images' + data.item.image" alt="">
</template>
</table>
// Vue JS
<script>
export default {
props:['posts'],
data: function() {
return {
imageList: []
};
},
mounted() {
this.fetch_image_list();
},
methods: {
fetch_image_list() {
let items = [];
if (Array.isArray(posts.data) && posts.data.length) {
posts.data.forEach((image,key) => {
let currentImage = {
'id':post.id,
'name':post.name,
'image':post.img,
}
items.push(currentImage)
});
this.imageList = items;
}
}
}
}
</script>
You should use this when accessing your data (if you don't have another scope defined inside). And you're trying to access the properties of undefined object (post) in forEach loop.
methods: {
fetch_image_list() {
let items = [];
if (Array.isArray(this.posts.data) && this.posts.data.length) {
this.posts.data.forEach((post, key) => {
let currentImage = {
'id':post.id,
'name':post.name,
'image':post.img,
}
items.push(currentImage)
});
this.imageList = items
}
}
}

Avoid unnecessary http requests on identical images - vuejs

Situation:
In a page, there are several components that receive a list of users. After receiving the list, there's a foreach cycle that calls an aditional component to fetch the user's image. It's possible that the several components may contain the same user, which would mean repeating the exact same http request's to fetch a "repeated image". To avoid these unecessary requests, I set the information of a user has a certain base64 image in the store of vueX, so that I can validate if I already got the image.
Problem: Happens that when the first component makes the request to fetch the image and save it in the store, the remaining components have already been created and as such, the store is still empty and I can't check if I have the image.
Solution: When I create the component, I force the store to exist by using
this.images[this.user.id] = 'reserved';
However, I'm not sure if this is the right approach to this situation.
Suggestions accepted :'D
Code:
parent component
<template>
<div class="info-cards">
<div class="info-users">
<div class="info-label">{{ $t('global.users') }}</div>
<div class="info-images" v-if="users.length > 0">
<base-users-image
v-for="user in users"
:key="user.name"
:user="user"
/>
</div>
<div v-else class="message">{{ $t('global.noUsersRole') }}</div>
</div>
</div>
</template>
<script>
// import components
const baseUsersImage = () => System.import(/* webpackChunkName: 'usersImage' */ './../../users/baseUsersImage');
export default {
props: {
users: Array,
packages: Array
},
components: {
baseUsersImage: baseUsersImage
},
}
</script>
image component
<template>
<router-link to="user" class="anchor-image">
<img v-if="show" :src="image" :alt="user.name" class="image">
<div v-else class="image-default">t</div>
</router-link>
</template>
<script>
// import requests
import requests from './../../../helpers/requests.js';
// import store
import { mapGetters, mapActions } from 'vuex';
export default {
props: {
user: Object
},
data() {
return {
image: '',
show: false
}
},
created() {
if (this.user.avatar) { // check if user has avatar
if ( this.images[this.user.id] == null) { // check if it already exists in the store
this.images[this.user.id] = 'reserved'; // set as reserved in store
requests.get(this.user.avatar, { responseType: 'arraybuffer' }) // faz o pedido a API da image
.then( (response) => {
this.saveImage( { id: this.user.id, url: `data:${response.headers['content-type']};base64,${Buffer.from(response.data, 'binary').toString('base64')}` } );
}, error => {
console.log(error);
});
}
}
},
methods: {
...mapActions({
saveImage: 'saveImage'
})
},
computed: {
...mapGetters({
images: 'images'
})
},
watch: {
images: {
immediate: true,
deep: true, // so it detects changes to properties only
handler(newVal, oldVal) {
if ( newVal[this.user.id] !=='reserved'
&& this.user.avatar
&& newVal[this.user.id] !== undefined
) {
this.image = newVal[this.user.id];
this.show = true;
}
}
}
}
}
</script>
store
const state = {
images: {}
}
const SAVE_IMAGE = (state, payload) => {
state.images = {
...state.images,
[payload.id] : payload.url
}
}
const saveImage = ({commit}, payload) => {
commit('SAVE_IMAGE', payload);
}
Here is what I would do:
First, I would move all the request logic to VueX and keep my component as simple as possible. It should be achievable by this piece of code:
export default {
props: {
user: Object
},
created () {
if (this.user.avatar) {
this.$store.dispatch('fetchImage', this.user.avatar)
}
}
}
Then, I would use this simple pattern to organize my store. First, let's take a look at how the state should look:
{
images: {
'/users/1/avatar': 'data:png:base64,....', // An image that have been loaded
'/users/2/avatar': null // An image that is supposed to be loading
}
}
As you can see, the images object uses images urls as keys and base64 data as value. If the value of the data is null, it means that the image is already loading.
Let's now see how do we write the action to handle that:
const actions = {
fetchImage ({state, commit}, url) {
if (typeof state.images[url] !== 'undefined') {
return null
}
commit('setImage', {
url,
payload: null
})
return requests.get(url, { responseType: 'arraybuffer'}).then(response => {
commit('setImage', {
url,
payload: `data:${response.headers['content-type']};base64,${Buffer.from(response.data, 'binary').toString('base64')}`
})
})
}
}
Look at the first condition. If the image is not undefined in the store, we just don't do anything. Because if the image is not undefined, it means that it is either null (loading) or has a value and is loaded.
Just after this condition, we set the image to null to prevent other components to load the image.
And at the end we load the content of the image, and commit it to the state.
Let's take a look to the template now:
<template>
<router-link to="user" class="anchor-image">
<img v-if="$store.state.images[user.avatar]" :src="$store.state.images[user.avatar]" :alt="user.name" class="image">
<div v-else class="image-default">t</div>
</router-link>
</template>
In order to check if you should display the image, you just have to use v-if="$store.state.images[user.avatar]". The image will show up as soon as it is loaded.
$store.state.images[user.avatar] will be falsy even if the image is loading (it has the null value.
I hope this can help!
(Here is the complete store:)
const store = {
state: {
images: {}
},
mutations: {
setImage (state, image) {
Vue.set(state.images, image.url, image.payload)
}
},
actions: {
fetchImage ({state, commit}, url) {
if (state.images[url] !== undefined) {
return null
}
commit('setImage', {
url,
payload: null
})
return requests.get(url, { responseType: 'arraybuffer'}).then(response => {
commit('setImage', {
url,
payload: `data:${response.headers['content-type']};base64,${Buffer.from(response.data, 'binary').toString('base64')}`
})
})
}
}
}

Resources