How to save data in to cache come from rest API with Axios in Vue 3 with option API - vuex

How to avoid duplicate API requests in Vue 3 with option Api. That means cache the data in the browser when first visited the API

You might want to take a look at localStorage, it allows you to store data in the browser (only strings, but JSON.stringify and JSON.parse are your friend) over multiple sessions. You do have direct access to the window.localStorage-object in every browser environment (not NodeJS), so it can totally be used with front-end frameworks like vue.js.
You can find a very minimal example at Codesandbox:
if (localStorage.getItem("result")) {
this.result = JSON.stringify(localStorage.getItem("result"));
} else {
const data = fetch("YOUR_URL").then(async (res) => {
const json = await res.json();
localStorage.setItem("result", JSON.stringify(json));
this.result = json;
});
}

Related

FirebaseAuth with SvelteKit on +page.ts load

I have a SvelteKit app and am using Firebase and Node to do simple Google SSO auth. I am using an API that requires the IDToken of the currently signed in user to authenticate requests. Ideally I'd like to use the +page.ts load function to load in the data, something like this:
export const load = (async () => {
// Get user, token
const auth = getAuth();
const user = auth.currentUser;
const token = await user?.getIDToken();
if (!token) throw error(401, "Could not authenticate");
// Use token to get data needed to load page
const data = api.requestData(token);
return { data };
}) satisfies PageLoad;
export const ssr = false;
The issue is that user is always null when this function executes. I imagine this is because this is called before the page loads and Firebase hasn't had a way to access the session and get the current user.
My question is, what approach do I take to solve this without simply requesting the data after the page is rendered? Is there a way to authenticate the user server side? Thanks so much.

404 error calling GET API from a shopify theme-extension

I'm new to shopify development, and can't figure out how to call an authenticated API from a shopify theme-extension. Essentially, I'm trying to make a theme extension, where one of the functionalities is that when a checkbox is clicked, an API that counts the number of products is called.
I have a working api that gets the product count, and in web>index.js, I have set-up the end-point:
app.get("/api/products/count", async (_req, res) => {
const countData = await shopify.api.rest.Product.count({
session: res.locals.shopify.session,
});
res.status(200).send(countData);
});
Under web>frontend>hooks, I have the authenticated hooks set-up as shown below. I've tested that if I call the "api/products/count" API from one of the web pages using useAppQuery, it works as expected, and returns the product count.
import { useAuthenticatedFetch } from "./useAuthenticatedFetch";
import { useMemo } from "react";
import { useQuery } from "react-query";
export const useAppQuery = ({ url, fetchInit = {}, reactQueryOptions }) => {
const authenticatedFetch = useAuthenticatedFetch();
const fetch = useMemo(() => {
return async () => {
const response = await authenticatedFetch(url, fetchInit);
return response.json();
};
}, [url, JSON.stringify(fetchInit)]);
return useQuery(url, fetch, {
...reactQueryOptions,
refetchOnWindowFocus: false,
});
};
In my theme extension code, I've added an event listener to the checkbox which calls getProductCount. In getProductCount, I want to call /api/products/count:
import { useAppQuery } from "../../../web/frontend/hooks";
export const getProductCount = (product) => {
const {
data,
refetch: refetchProductCount,
isLoading: isLoadingCount,
isRefetching: isRefetchingCount,
} = useAppQuery({
url: "/api/products/count",
reactQueryOptions: {
onSuccess: () => {
setIsLoading(false);
},
},
});
}
However, when I run locally and click the checkbox, it returns a 404 error trying to find useAppQuery. The request URL is https://cdn.shopify.com/web/frontend/hooks. It seems like the authentication isn't working because that URL looks incorrect.
Am I missing a step that I need to do in order to call an authenticated API from a theme-extension?
I thought the issue was just the import path for useAppQuery but I've tried different paths, and they all return the same 404 issue.
If you want a hot tip here. In your theme App extension, you do not actually need to make an API call to get a product count. In your theme app extension, you can just use Liquid, and dump the product count out to a variable of your choice, and use the count, display the count, do whatever.
{{ shop.product_count }}
Of course, this does not help you if you need other storefront API calls in your theme App extension, but whatever. In my experience, I render the API Access Token I need in my theme app extension, and then making my Storefront API calls is just a fetch().
The only time I would use authenticated fetch, is when I am doing embedded App API calls, but that is a different beast from a theme app extension. In there, you do not get to make authenticated calls as the front-end is verboten for those of course. Instead you'd use App Proxy for security.
TL:DR; Storefront API calls with a token should not fail with a 404 if you call the right endpoint. You can use Storefront API inside a theme app extension. Inside a theme app extension, if you need backend Admin API access, you can use App Proxy calls.

React Query uses outdated headers (old JWT token) after Keycloak refresh token

I'm pretty new in React-Native programming, but here is the context.
We are using React Query and Axios libraries in our project. As AuthManager we are using Keycloak and for the library managing auth status we have React Native Keycloak. We encounter a tedious problem with our server responding randomly 401 at our requests after a certain amount of time, bringing also to the app crash sometimes.
We reproduced the error making the Bearer Token of Keycloak expire after only 1 minute. This caused almost immediatly the 401 error and we wondered why this is happening.
Let's say we have a screen with some "Activities" and this screen is the first thing the user will see. For handling requests, in our code we use some custom hooks that reference useQuery, for example:
export function useActivities(): UseQueryResult<ActivityList> {
const { headers } = useHeaders();
return useQuery(
['activities', today.start],
() => getActivitiesList(headers), // Note 1
{
enabled: !!today.start,
}
);
}
The important point of it is that we useHeaders to get our updated headers with the Keycloak token and our realm settings. useHeaders is almost everywhere in our app.
export function useHeaders(): UseHeaders {
const { keycloak } = useKeycloak();
const KEYCLOAK_REALM = remoteConfig().getString('KEYCLOAK_REALM');
const headers = {
Authorization: `Bearer ${keycloak?.token}`,
Realm: KEYCLOAK_REALM,
};
return { headers };
}
Now, the getActivitiesList is simple as five:
async function getActivitiesList(headers: UseHeaders['headers']): Promise<ActivityList> {
const url = `${BASE_URL}${VERSION}/activities/grouped?end=${end}&start=${start}`;
// Note 2
return axios
.get(url, { headers })
.then((res) => res.data)
.catch((e) => console.error('Error fetching grouped activities:', e));
}
The problem with all of that is that whenever Keycloak will trigger the refresh token, the token inside keycloak object is changed, the headers inside useActivities are changed BUT if I print the headers inside my getActivitiesList (// Note 2), or even inside the query function (// Note 1), headers will not be updated. Sometimes it just causes to make two requests (one that fails and show error, the other one actually working), some other times the app crashes without any explain. This makes me wonder why the query function will not update its headers and passed the "old" headers inside getActivitiesList.
For now we are mitigating this problem in two different points.
After keycloak init, we pass immediatly to axios a global header with axios.defaults.headers.common.Realm = KEYCLOAK_REALM;
After receiving a valid token from Keycloak, we overwrite the Authorization header with a new one: axios.defaults.headers.common.Authorization = 'Bearer ${keycloak?.token}';
This is not a perfect solution and we are here to search some info about this problem.
Someone get something similar? How to manage the token refresh in useQuery function?

How to use a private API key with Nuxt (on the client)?

Problem Solved
If you're struggling with the same issue, look at the accepted answer which is one way to achieve it by using serverMiddleware
I'm using an API which required a private key. I've stored the key inside a .env file, and called it in the nuxt configuration file, like this :
privateRuntimeConfig: {
secretKey: process.env.MY_SECRET_KEY
},
My API call is done inside the asyncData() hook on my index page. It works fine when i load this page, or reload it, but everytime i use the navigation to come back to this page, i end up with an error (I use a buffer to convert my API key to base64)
First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.
After some research and debugging, i found out that my private key wasn't available at the time, and the "secret" value used in my api call was "undefined".
The thing I don't get is why is this working on initial load / reload but not on page navigation ? And is there a way to fix it without using a backend ? (SSR for SEO and the ability to use private keys without exposing them are the main reasons why i used Nuxt for my project)
Here is my code :
async asyncData({ $content, store, $config }) {
const secret = Buffer.from($config.secretKey).toString('base64')
const request = await fetch('https://app.snipcart.com/api/products', {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
})
const result = await request.json()
store.commit('products/addProducts', result)
const stocks = store.getters['products/getProducts']
return { stocks }
},
Update
Looking at the #nuxtjs/snipcart module's key key and since it's a buildModules, you can totally put it there since it will be available only during the build (on Node.js only)!
For more info, Snipcart do have a lot of blog posts, this one based on Nuxt may help clearing things up: https://www.storyblok.com/tp/how-to-build-a-shop-with-nuxt-storyblok-and-snipcart
You do have your key initially because you're reaching the server when you enter the page or hard refresh it.
If you navigate after the hydration, it will be a client side navigation so you will not be able to have access to the private key. At the end, if your key is really private (nowadays, some API provide keys that can be exposed), you'll need to work around it in some ways.
Looking at Snipcart: https://docs.snipcart.com/v3/api-reference/authentication, it clearly states that the key should be available in
Appear in your compiled front-end assets (HTML, JavaScript)
Meanwhile, if you need to make another call to your backend (trying to access something else than products), you'll need to make a second call.
With Nuxt2, you cannot reach for the backend each time as of right now since you will stay in an SPA context (Nuxt is a server then client Vue app basically). But you could write down the token into a cookie or even better, use a backend as a proxy to hide this specific key (or even a serverless function).
Some more info can be found on my other answer here: https://stackoverflow.com/a/69575243/8816585
Thanks #kissu for your (very) quick answer :)
So, based on what you said and your other answer on the subject, i've made a server Middleware in Nuxt in my server folder.
server/snipcart.js
const bodyParser = require('body-parser')
const axios = require('axios')
const app = require('express')()
app.use(bodyParser.json())
app.all('/getProducts', (request, response) => {
const url = 'https://app.snipcart.com/api/products'
const secret = Buffer.from(process.env.SNIPCART_SECRET).toString('base64')
const config = {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
}
axios
.get(url, config)
.then(res => {
const products = {}
res.data.items.forEach(
item => {
const productId = item.userDefinedId.replace(/-/g, '')
const stocks = {}
item.variants.forEach(
variant => {
const size = variant.variation[0].option
const stock = variant.stock
stocks[size] = stock
}
)
products[productId] = stocks
}
)
response.json(products)
})
.catch( err => response.json(err) )
})
module.exports = app
Correct me if i'm wrong, but I think that's basically the same as using a server as a proxy right ? Based on Nuxt lifecycle hooks, the serverMiddleware one is only run on the server, so my API key shouldn't be exposed to the client ? (I still need to do some refactoring to clean the code, but at least it's working) (https://nuxtjs.org/docs/concepts/nuxt-lifecycle/#server & https://nuxtjs.org/docs/configuration-glossary/configuration-servermiddleware/)
nuxt.config.js
serverMiddleware: [
{ path: "/server", handler: "~/server/snipcart.js" }
]
index.vue (where my snipcart API call was previously made, i guess now I should move this call directly from the product card component where the data is needed) :
async asyncData({ $content, store, $axios }) {
await $axios
.get('/server/getProducts')
.then(res => store.commit('products/addProducts', res.data))
.catch(err => console.log(err))
const stocks = store.getters['products/getProducts']
return {stocks, masterplanProducts }
},
PS : Snipcart does provide a public API key, but the use is very limited. In order to access the remaining stock for each product, i have to use the private key (which allows for some other operations, like removing products / accessing orders and such)
UPDATE :
It's not working when the website is fists accessed from any other page than the one one where the API call is, since the store won't have any data from the API call)
Okay, now I feel dumb. I found a way to make it work. I guess taking the time to explain my problem helped me understand how to solve it.
For those who encounter a similar issue, i fixed it by wrapping my API call with a If statement.
if ($config.secretKey) {
const secret = Buffer.from($config.secretKey).toString('base64')
const request = await fetch('https://app.snipcart.com/api/products', {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
})
const result = await request.json()
store.commit('products/addProducts', result)
}
const stocks = store.getters['products/getProducts']
This way, i can just skip the API call and access values from my vuex store.

How to minimize data traffic in vuejs

At work, we think about using Vuejs with Vuex for our frontend. I have already used it for private projects.
One of our big questions is the traffic. Our platform needs a lot of data, sometimes in large packages. Some of them are really big and it would be more expensive to load them more than once.
I've seen many examples of vue with vuex, but for me it looked like all the samples would request the same (already loaded) data every time they paged.
My real question is: Is there a way in vuejs or general to avoid or solve this problem? The only helpful thing I have found so far was this.
As far as I know, you can use this library https://github.com/kuitos/axios-extensions
An example here
import Axios from 'Axios';
import { throttleAdapterEnhancer } from 'axios-extensions';
const http = axios.create({
baseURL: '/',
headers: { 'Cache-Control': 'no-cache' },
adapter: throttleAdapterEnhancer(axios.defaults.adapter, { threshold: 2 * 1000 })
});
http.get('/users'); // make real http request
http.get('/users'); // responsed from the cache
http.get('/users'); // responsed from the cache
setTimeout(() => {
http.get('/users'); // after 2s, the real request makes again
}, 2 * 1000)
As you can see, you can create an adaptor and custom what you want. For example here, you keep the cache for only 2 seconds. So the first request to /users is a real one. The second and thirst request are cache, and the last one is after the two seconds, so it's a real one.
Can you be more specific about the context of how you will keep the cache. When do you need to reload the cache, after how many times, after which event, ...?
The strategy I use is to store the value on the Vuex state.
I write all my request in Vuex actions. In every action, I check if the data already exists on the Vuex state. If it does, I simply bypass the request and return the data from the state (saving requests from being called multiple times). If it doesn't exist, I'll make the request, then store the result on the Vuex state, and return the value.
Vuex Action:
getLists({ state, commit }) {
return new Promise((resolve, reject) => {
if (state.isSetLists === false) {
getListsFromServer((error, data) => {
if (error) {
reject(error);
} else {
console.log('call to getLists successful:', data);
commit('setLists', data.lists);
resolve(data.lists);
}
});
} else {
resolve(state.lists);
}
});
},
Then, the setLists mutation handles it like so:
setLists(state, lists) {
state.isSetLists = true;
state.lists = lists;
},
This way, the user can page around all they want, and only ever call each request once.