Nuxt 3 useAsyncData access previous value with refresh method - server-side-rendering

I am trying to implement infinite scroll using nuxt3.
Key target:
Initial load should contain server rendered content
So my code is
const { data, refresh } = await useAsyncData(async () => {
const response = await $fetch(`/api/whatever?limit=${limit}&skip=${skip}`);
return {
totalElements: response.count,
items: response.items,
};
});
Everything is fine here except I couldn't accumulate previous result with current result after refresh() to achieve infinite scroll because data is not accessible inside useAsyncData handler so i can't use
return {
totalElements: response.count,
items: [...data.value.items, ...response.items],
};
Also it is impossible to assign current value to ref and reuse it inside handler as server ref will take changes and client ref will be empty value at initial load.
I have a workaround but it is a kind of a crutch and sometimes results to a glitchy scroll behaviour because server rendered data.value.items is an array and after modifying data.value.items on client side it becomes an array of proxies.
// This code is outside useAsyncData composable
...
const response = await useFetch(...);
data.value.items = [...data.value.items, ...response.items];
Any ideas?

Related

using watch function w/prop in Vue3 composition api

I have a component that renders a table of Inventoried computer equipment. Here is the relevant code for initial render:
let oEquiptByType = reactive({
Laptop: [],
iPad: [],
"Document Camera": [],
"Overhead Projector": [],
Chromebook: [],
Desktop: [],
MacBook: [],
Scanner: [],
});
// ======== Props =========== //
const props = defineProps({
propFormData: {},
});
// Now let's use Stein to retrieve the SS data
// eslint-disable-next-line no-unused-vars
const fetchSheetsData = function () {
const store = new SteinStore(
"https://api.steinhq.com/v1/storages/618e81028d29ba2379044caa"
);
store
.read("HS - Classrooms")
.then((data) => {
scrapDataHSClassrooms.value = data;
emptyRowsRemoved.value.forEach((item) => {
// Let's construct an object that separates equipment by type
// Check if property exists on oEquiptByType object
const exists = Object.prototype.hasOwnProperty.call(
oEquiptByType,
item["Equipment"]
);
// If item(row) is good lets push the row onto the corresponding Object Array
// in oEquiptByType. This will construct an object where each object property corresponds
// to an equipment category. And each oEquiptByType entry is an array where each array
// element is a row from the SS. e.g., oEquiptByType["Laptop"][3] is a row from
// SS and is a laptop.
if (exists) {
oEquiptByType[item["Equipment"]].push(item);
}
});
})
.catch((e) => {
console.error(e);
failure.value = true;
});
};
// =============== Called on component mount =============================== //
onMounted(fetchSheetsData);
The initial render is fine. Now I have a watcher on the prop so when someone submits a new item for the inventory I push that data onto the corresponding object array (ie, a laptop would be pushed onto the oEquiptByType[props.propFormData.Equipment] via oEquiptByType[props.propFormData.Equipment].push(props.propFormData);
// ================================================================ //
// ======================= Watch effects ========================== //
// ================================================================ //
watch(props.propFormData, () => {
// Push the submitted form item onto the reactive
// oEquiptByType object array. This update of Vue state
// will then be injected into DOM and automagically update browser display.
oEquiptByType[props.propFormData.Equipment].push(props.propFormData);
});
This works fine for the first item I add to backend as you can see here with original and then adding first item :
and after first item added (a laptop)
Notice the oEquiptByType[props.propFormData.Equipment] has the new item added. Great.
But now when I add a second item (a MacBook) is added this is resulting state:
Notice the Macbook array has been updated but also the Laptop array's last item has been overwritten with the Mac book entry??? And this behavior continues for any additional items added from a user. I have read docs over and do not see anything that would explain this behavior. I'm hoping maybe someone with more than my limited experience with Vue can help me out. Any additional info needed please let me know. Thanks...
Update:
Put a JSON.Stringify in watch function
Update two:
here is lineage of prop.FormData-
we start in form-modal and emit the form data like:
emit("emiterUIUpdate", formAsPlainObject);
then catch the data in the parent App.vue:
<FormModal
v-show="isModalVisible"
#close="closeModal"
#emiterUIUpdate="updateUI"
>
<DisplayScrap :propFormData="formData" />
const formData = reactive({});
// Method to be called when there is an emiterUIUpdate event emiited
// from form-modal.vue #param(data) is the form data sent from the
// form submission via the event bus. We will then send this data back
// down to child display-scrap component via a prop.
const updateUI = (data) => {
Object.assign(formData, data);
};
and then as posted previous in display-scrap.vue the prop propFormData is defined and watched for in the watch function. hope that helps..
It seems like the watch is getting triggered more often than you expect.
Might be that changes to props.propFormData are atomic and every incremental change triggers changes to the props, which in turn triggers the watch.
Try console logging the value of props.propFormData with JSON.stringify to see what changes are triggering it.
What happens here:
Your form modal emits the emiterUIUpdate event on Ok or Save (button)
Parent takes the object emitted and use Object.assing to copy all properties of emitted object to a formData reactive object. Instead of creating completely new object, you are just replacing the values of all properties of that object all and over again
The formData object is passed by a prop to child component and whenever it changes, it is pushed to target array
As a result, you have a multiple references to same object (formData hold by a parent component) and all those references are to same object in memory. Every Object.assign will overwrite properties of this object and all references will reflect those changes (because all references are pointing to the same object in memory)
Note that this has nothing to do with Vue reactivity - this is simple JavaScript - value vs reference
There is no clear answer to what to do. There are multiple options:
Simplest (and not clearest)
just do not use Object.assign - create new object every time "Save" is clicked
change formData to a ref - const formData = ref({})
replace the value of that ref on emiterUIUpdate event - formData.value = { ...data }
your watch handler in the child will stop working because you are watching props in a wrong way - instead of watch(props.propFormData, () => { use watch(() => props.propFormData, () => {
Better solution
the data should be owned by parent component
when modal emits new data (Save), Parent will just add the newly generated object into a list
share the data with DisplayScraps component using a prop (this can be a simple list or a computed creating object similar to oEquiptByType)

TTFB is taking so long (15s-20s) for simple NextJS page in Firebase production

I have a simple page that is applying SSR as follows:
const page = ({initProps}) => {
// render some static texts
// render images
};
page.getInitialProps = async (ctx) => {
// get id from ctx
// get data from Firestore (get by id, no aggregation)
const firebaseRes = await db.collection("organizations")
.doc(id)
.get();
// return data
}
Currently, in the production environment, it takes around 15s for TTFB.
I tried a lot of things (use next/image, reduce the data amount returned by getInitialProps...) to reduce the latency time but no luck.
Is there anything else I can check/improve for my case?
==========
Add more information:
I run my app as a Firebase function
My page is a landing page (static text, static images, dynamic image loading, one Lottie animation)
I'm using TailwindCSS
My NextJS version is 12.x
Inside the initialProp function, I connect to Firestore directly to get the data.
Inside the initialProp function, besides querying the data, I have a signInWithEmailAndPassword to get the token.

Local storage is not defined

How can I get values from local storage in next.js?When i give localStorage.getItem() in console,it is prnting the values.But when I assign this to a variable it is giving LocalStorage is not defined error.I have also added redux-persist in my localstorage
localStorage.getItem('id')
Local Storage is a Web API native to modern web browsers. It allows websites/apps to store data in the browser, making that data available in future browser sessions.
There are two React lifecycle methods we can use in our component to save/update the browsers localStorage when the state changes:
componentDidMount()
componentDidUpdate()
componentDidMount will run once your component has become available and loaded into the browser. This is when we gain access to localStorage. Since localStorage doesn’t reside in Node.js/Next.js since there is no window object, we will have to wait until the component has mounted before checking localStorage for any data. So If you want to assign the local storage value into a variable, please do this inside the componentDidMount method.
componentDidMount() {
const data = localStorage.getItem('id')
console.log(data);
if(data) {
//here you can set your state if it is necessary
}
}
And If we want to update our local storage value through the state we can easily update the localStorage value with our changes value by using componentDidUpdate. This method gets run each time the state changes so we can simply replace the data in localStorage with our new state.
componentDidUpdate() {
localStorage.setItem('id', JSON.stringify(this.state))
}
localStorage is a property of object window. It belongs to the browser, not next.js nor React, and accessing localStorage is not possible until React component has been mounted. So you need to ensure that your React app is mounted before calling localStorage, e.g. calling localStorage.getItem inside componentDidMount.
When working with a framework like Next.js that executes code on the server side, using localStorage produces an error like "localStorage is not defined" or "window is not defined"
To fix this, check to see if window is defined so that the code will run only when it's available.
This is a great article that explains more: https://blog.logrocket.com/using-localstorage-react-hooks/
See the section called, "Problems accessing localStorage for an SSR application"
You can create a file called "useLocalStorage.tsx" or whatever, and it would contain something like this:
import { useState, useEffect } from "react";
function getStorageValue(key, defaultValue) {
// getting stored value
if (typeof window !== 'undefined') {
const saved = localStorage.getItem(key);
return saved || defaultValue;
}
}
export const useLocalStorage = (key, defaultValue) => {
const [value, setValue] = useState(() => {
return getStorageValue(key, defaultValue);
});
useEffect(() => {
// storing input name
localStorage.setItem(key, value);
}, [key, value]);
return [value, setValue];
};
Then you can just import it into the file you want to use it in like this:
import { useLocalStorage } from './useLocalStorage'
Then you can call it to get the "id" from localStorage:
const [id, set_id] = useLocalStorage("id", "");
First think to take a note is, localStorage has nothing to do with next.js or redux-persist. localStorage is the internal window object and can be directly accessible without any definition.
I think you are trying to access the localStorage before it is being set, so you get that error.
Simple solution to this is to use Conditional (ternary) operator
,
const id = localStorage.getItem('id') ? localStorage.getItem('id') : "set your own default value";
console.log(id);

option.timeout ignored waiting for Selector.withAttribute

I have tried this every which way, but I can't get TestCafe to wait for the disabled attribute to be removed from an element.
This obviously blocks all further testing, since I need the button to be clickable before I can proceed in the flow.
fixture('create').page('locahost:3000');
test('one', async => {
const myIframe = Selector('#myIframe');
await t
.typeText('#input', 'words')
.click('#update')
.expect(myIframe.exists).ok('', { timeout: 10000 })
.switchToIframe(myIframe)
const activeStartButton = await Selector('#start').withAttribute('disabled');
await t
.expect(activeStartButton).notOk('', { timeout: 60000, allowUnawaitedPromise: true });
});
Regardless of whether I defined activeStartButton ahead of time, or add or remove await from the definition, put the selector directly in expect with or without await, separate this await block from the previous one or add it to the previous chain, TestCafe immediately throws an error atexpect(activeStartButton).notOk`
The error varies depending on my approach, but for this code:
AssertionError: start button remains disabled: expected [Function: __$$clientFunction$$] to be falsy"
Your code should look like this:
const selector = Selector('#start')
.with({visibilityCheck: true});
await t
.expect(selector.exists).ok({timeout: 10000}) // ensure the button is visible on the screen
.hover(selector) // access to the button via the mouse
.expect(selector.hasAttribute("disabled")).notOk({timeout: 10000}) // ensure the field is enabled
.click(selector);
Maybe you should also have a look to say goodbye to flakyness
This code:
const mySelector = Selector('any css selector');
await t
.expect(mySelector).notOk()
will always throw an error because the truthiness of mySelector is always true. So the above code is similar to this code:
assert(true).toBe(false).
Above mySelector is a promise object and the truthiness of a promise is always true.
Now if you write:
const mySelector = await Selector('any css selector');
await t
.expect(mySelector).notOk();
mySelector is a NodeSnaphsot object which is some sort of literal object with plenty of properties on it like:
{
textContent,
attributes,
id,
clientHeight,
...
}
The truthiness of a literal object is always true, and therefore the above
expect will still throw an error.
In fact, this problem could have been completely masked if the test code was instead:
const mySelector = await Selector('any css selector');
await t
.expect(mySelector).ok();
The above test code will always pass even if mySelector does not represent any existing element in the DOM.
Inside the expect you should assert only for a property or for a method of the Selector that returns a boolean value when using ok() or notOk().
Possible boolean properties are:
mySelector.hasChildElements
mySelector.hasChildNodes
mySelector.checked
mySelector.focused
mySelector.selected
mySelector.visible
mySelector.exists
Possible methods are:
mySelector.hasClass('className')
mySelector.hasAttribute('attributeName')
The `.withAttribute('attributeName') is just a filter method that returns a Selector object (i.e. a Promise) and the truthiness of this result is always true.
So when you are writing :
const mySelector = Selector('any css selector').withAttribute('attributeName');
it's more or less like writing this pseudo-code:
const mySelector = Selector('any css selector') // returns a collection of Selectors
.toArray() // convert it to an array
.filter((selector) => selector.hasAttribute('attributeName'))
.toPromise() // convert back to a promise object

Meteor.connection._lastSessionId is undefined in onCreated

So I have this code guys
Template.mainLayout.onCreated(function () { //HERE
console.log("mainLayout created");
var context = FlowRouter.current();
// use context to access the URL state
console.log(context);
var visitedOne = context.path;
//getting the connID
var clientIp = headers.getClientIP(); // no need for this anymore
var clientConnId = Meteor.connection._lastSessionId; // HERE
console.log(clientIp);
console.log(clientConnId); //HERE
// console.log(Meteor.connection._lastSessionId);
Meteor.call("updateHistory", {clientIp,clientConnId,visitedOne}, function(error, result){
if(error){
console.log("error", error);
});
if(result){
}
});//Meteor.call
});
My problems are marked by the comments //HERE
Meteor.connection._lastSessionId returns undefined at onCreated event. However if I try to get on click event it works just fine. Why is this caused, what's a workaround for this?
You're attempting to log the session ID before the connection has received it. For example, wrap your call in a setTimeout:
...
setTimeout(() => {
console.log(Meteor.connection._lastSessionId);
}, 500);
...
You might have to tweak the timeout value a bit, but it will be logged. Using setTimeout in this fashion really isn't that reliable though, as the amount of time it takes for the session ID to get set can vary. You'll likely want to look into setting up some kind of simple polling to continuously check for the session ID, until it's set.
Basically _lastSessionId isn't yet available on the client when the template is originally created (it's probably the first template rendered in your app). However there is no need to get this on the client since you're calling a server method anyway, just use the variable directly there where it will already exist!
So simplify:
Meteor.call("updateHistory", {clientIp,clientConnId,visitedOne}, callback)
to:
Meteor.call("updateHistory", visitedOne, callback)
and get the clientIp (if necessary) and use this.connection.id on the server.

Resources