How to make modal have false value after being opened once - vue.js

Whenever I load my component, I load a modal with instruction, I want it to happen only once. When user press f5 i don't want to show this modal anymore unless he triggers it with a button. How can I set this up, do I need a computed function with localStorage?
component
InstModal(:toggleModal="instructionModal" #ok="instructionModal = false")
setup
const instructionModal = ref(false)
setTimeout(() => (instructionModal.value = true), 2000)

You need localStorage, so that you can persist this across different sessions. You don't need computed to acheive what you want, tho. Let's say you want to store it in localStorage with a key of instructionModalOpened:
const instructionModal = ref(+localStorage.getItem('instructionModalOpened') || false)
setTimeout(() => {
instructionModal.value = true;
localStorage.setItem('instructionModalOpened', '1')
}, 2000);
However, if instructionModal can be mutated in other places of your code, you are better off watching it, i.e.:
const instructionModal = ref(+localStorage.getItem('instructionModalOpened') || false)
setTimeout(() => {
instructionModal.value = true;
}, 2000);
watch(instructionModal, (val) => {
if (!val) return;
localStorage.setItem('instructionModalOpened', '1')
});

Related

Vuejs v-for so laggy when infinite scrolling

I have this weird vuejs effect where when I am adding a new object data, the v-for re-renders all the object even if its already rendered.
I am implementing an infinite scroll like facebook.
The Code
To explain this code, I am fetching a new data from firebase and then push the data into the data object when it reaches the bottom of the screen
var vueApp = new Vue({
el: '#root',
data: {
postList: [],
isOkaytoLoad: false,
isRoomPostEmpty: false,
},
mounted: function() {
// Everytime user scroll, call handleScroll() method
window.addEventListener('scroll', this.handleScroll);
},
methods: {
handleScroll: function()
{
var d = document.documentElement;
var offset = d.scrollTop + window.innerHeight;
var height = d.offsetHeight - 200;
// If the user is near the bottom and it's okay to load new data, get new data from firebase
if (this.isOkaytoLoad && offset >= height)
{
this.isOkaytoLoad = false;
(async()=>{
const lastPost = this.postList[this.postList.length - 1];
const room_id = PARAMETERS.get('room');
const q = query(collection(db, 'posts'), where('room', '==', room_id), orderBy("time", "desc"), limit(5), startAfter(lastPost));
const roomSnapshot = await getDocs(q);
roomSnapshot.forEach((doc) => {
const postID = doc.id;
(async()=>{
// Put the new data at the postList object
this.postList = [...this.postList, doc];
const q = query(collection(db, 'comments'), where('post_id', '==', postID));
const commentSnapshot = await getDocs(q);
doc['commentCount'] = commentSnapshot.size;
//this.postList.push(doc);
console.log(this.postList);
setTimeout(()=>{ this.isOkaytoLoad = true }, 1000);
})();
});
})();
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div v-if="postList.length > 0" class="card-containers">
<!-- I have a component `Postcard` in my js file and it works well -->
<Postcard
v-for="post in postList"
:key="post.id"
:post-id="post.id"
:owner-name="post.data().owner_displayName"
:owner-uid="post.data().owner_id"
:post-type="post.data().post_type"
:image-url="post.data().image_url"
:post-content="truncateString(linkify(post.data().post_content))"
:room="post.data().room"
:time="post.data().time.toDate()"
:likers="post.data().likers"
:comment-count="post.commentCount"
:file-url="post.data().file_url"
:file-name="post.data().file_name"
:downloads="post.data().downloads">
</Postcard>
</div>
Now, the problem is here ...
Look at this screen record, FOCUS AT THE MOUSE, it's lagging and I can't even click on those buttons when vuejs is adding and loading the new data
Here is the code That I used
What I suspect
I am suspecting that everytime I add a new data, the VueJS re-renders it all, which does that effect. How can I force vueJS to not re-render those data that is already rendered in the screen?
You've got two unnecessary async IIFE; the second one inside the forEach is particularly problematic because the async code inside it will be executed concurrently for each loop iteration, which has implications:
getDocs() will be fired all at once for each loop ieration, potentially spamming the server (assuming this is performing a network request). Was this your intention? It looks like you're only fetching at most 5 new posts, so this is probably OK.
The async function updates some state which will trigger Vue to re-render for each doc. This should be batched together at the end so Vue does as minimal updates as possible.
Also don't use var; use const or let instead. There's almost no good reason to use var anymore, let it die.
I can't say this will improve your performance substantially, but I recommend making the following changes (untested):
async handleScroll() {
const d = document.documentElement;
const offset = d.scrollTop + window.innerHeight;
const height = d.offsetHeight - 200;
// If the user is near the bottom and it's okay to load new data, get new data from firebase
if (this.isOkaytoLoad && offset >= height) {
// Prevent loading while we load more posts
this.isOkaytoLoad = false;
try {
// Get new posts
const lastPost = this.postList[this.postList.length - 1];
const room_id = PARAMETERS.get('room');
const q = query(collection(db, 'posts'), where('room', '==', room_id), orderBy("time", "desc"), limit(5), startAfter(lastPost));
const roomSnapshot = await getDocs(q);
// Fetch comments of each post. Do this all at once for each post.
// TODO: This can probably be optimized into a single query
// for all the posts, instead of one query per post.
await Promise.all(roomSnapshot.docs.map(async doc => {
const postID = doc.id;
const q = query(collection(db, 'comments'), where('post_id', '==', postID));
const commentSnapshot = await getDocs(q);
doc.commentCount = commentSnapshot.size;
}));
// Append the new posts to the list
this.postList.push(...roomSnapshot.docs);
} catch (ex) {
// TODO: Handle error
} finally {
// Wait a bit to re-enable loading
setTimeout(() => { this.isOkaytoLoad = true }, 1000);
}
}
}
Doing :post-content="truncateString(linkify(post.data().post_content))" in the template means linkify will be executed during each re-render. I suspect linkify may be potentially slow for long lists? Can this be pre-calculated for each post ahead of time?
You're registering a window scroll event listener when the component is mounted. If the component is ever destroyed, you need to unregister the event listener otherwise it'll still fire whenever the window is scrolled. This may not be an issue in your case, but for reusable components it must be done.

When state changes for graphql variable, result stays the same on react-native

I'm trying to create an app using shopify graphql api to create an ecommerce app on react native expo.
I have an onPress that calls a setState to change the state of the graphQL variable but the results don't change from the initial state of 'currentSubCategories'
const [currentSubCategories, setSubCategories] = useState(Categories[0].subCategory[0].handle);
let {
collection,
loading,
hasMore,
refetch,
isFetchingMore,
} = useCollectionQuery(currentSubCategories, first, priceRange);
const [currentCategory, setCategory] = useState({categories: Categories[0]});
const onSubCategorySelect = (subCategory) => { setSubCategories(subCategory.handle) }
onPress={() => onSubCategorySelect(item)}
function useCollectionQuery(
collectionHandle: string,
first: number,
priceRange: [number, number],
) {
let [isInitFetching, setInitFetching] = useState<boolean>(true);
let [isReloading, setIsReloading] = useState<boolean>(true);
let [collection, setCollection] = useState<Array<Product>>([]);
let isFetchingMore = useRef<boolean>(false);
let hasMore = useRef<boolean>(true);
let defaultCurrency = useDefaultCurrency().data;
let { data, loading, refetch: refetchQuery } = useQuery<
GetCollection,
GetCollectionVariables
>(GET_COLLECTION, {
variables: {
collectionHandle,
first,
sortKey: ProductCollectionSortKeys.BEST_SELLING,
presentmentCurrencies: [defaultCurrency],
},
notifyOnNetworkStatusChange: true,
fetchPolicy: 'no-cache',
});
let getMoreUntilTarget = async (
targetAmount: number,
cursor: string | null,
handle: string,
filter: [number, number],
) => {
let result: Array<Product> = [];
let moreData: Array<Product> = [];
let { data } = await refetchQuery({
first,
collectionHandle: handle,
after: cursor,
});
...
useEffect(() => {
if (!loading) {
isFetchingMore.current = false;
}
if (isInitFetching && !!data && !!data.collectionByHandle) {
let newCollection = mapToProducts(data.collectionByHandle.products);
hasMore.current = !!data.collectionByHandle?.products.pageInfo
.hasNextPage;
setCollection(newCollection);
setIsReloading(false);
setInitFetching(false);
}
}, [loading, isInitFetching]); // eslint-disable-line react-hooks/exhaustive-deps
return {
collection,
loading: isReloading,
hasMore: hasMore.current,
isFetchingMore: isFetchingMore.current,
refetch,
};
}
I'm using flatList to show the result
<FlatList
data={collection}
renderItem={({ item }) => (
<Text>{item.title}</Text>
)}
/>
According to docs you have to pass new variables to refetch otherwise refetch will use initial values.
In this case (custom hook) you have 2 ways to solvethis problem:
return variables from your custom hook (taken from useQuery),
return some own refetch function.
1st option needs 'manual' variables updating like:
refetch( { ...variablesFromHook, collectionHandle: currentSubCategories } );
In 2nd case you can create myRefetch (and return as refetch) taking collectionHandle parameter to call refetch with updated variables - hiding 'complexity' inside your hook.
Both cases needs refetch call after updating state (setSubCategories) so you should use this refetch inside useEffect with [currentSubCategories] dependency ... or simply don't use state, call refetch directly from event handler (in onSubCategorySelect).

React Native State Management Question - when does useState hook load?

I have a FlatList of items that has a "remove" button next to it.
When I click the remove button, I am able to remove the item from the backend BUT the actual list item is not removed from the view.
I am using useState hooks and it was to my understanding that the component re-renders after setState happens.
The setState function is used to update the state. It accepts a new
state value and enqueues a re-render of the component.
https://reactjs.org/docs/hooks-reference.html
What am I missing with how state is set and rendering?
I don't want to use the useEffect listener for various reasons. I want the component to re-render when the locations state is updated....which I am pretty sure is happening with my other setStates....not sure if I am totally missing the mark on what setState has been doing or if it's something specific about setLocations().
const [locations, setLocations] = useState(state.infoData.locations);
const [locationsNames, setLocationsNames] = useState(state.infoData.names]);
...
const removeLocationItemFromList = (item) => {
var newLocationsArray = locations;
var newLocationNameArray = locationsNames;
for(l in locations){
if(locations[l].name == item){
newLocationsArray.splice(l, 1);
newLocationNameArray.splice(l, 1);
} else {
console.log('false');
}
}
setLocationsNames(newLocationNameArray);
setLocations(newLocationsArray);
};
...
<FlatList style={{borderColor: 'black', fontSize: 16}}
data={locationNames}
renderItem={({ item }) =>
<LocationItem
onRemove={() => removeLocationItemFromList(item)}
title={item}/> }
keyExtractor={item => item}/>
UPDATED LOOP
const removeLocationItemFromList = (item) => {
var spliceNewLocationArray =locations;
var spliceNewLocationNameArray = locationsNames;
for(f in spliceNewLocationArray){
if(spliceNewLocationArray[f].name == item){
spliceNewLocationArray.splice(f, 1);
} else {
console.log('false');
}
}
for(f in spliceNewLocationNameArray){
if(spliceNewLocationNameArray[f] == item){
spliceNewLocationNameArray.splice(f, 1);
} else {
console.log('false');
}
}
var thirdTimesACharmName = spliceNewLocationNameArray;
var thirdTimesACharmLoc = spliceNewLocationArray;
console.log('thirdTimesACharmName:: ' + thirdTimesACharmName + ', thirdTimesACharmLoc::: ' + JSON.stringify(thirdTimesACharmLoc)); // I can see from this log that the data is correct
setLocationsNames(thirdTimesACharmName);
setLocations(thirdTimesACharmLoc);
};
This comes down to mutating the same locations array and calling setState with the same array again, which means that the FlatList which is a pure component will not re-render since the identity of locations has not changed. You could copy the locations array to newLocationsArray first (similarly with the newLocationNameArray) to avoid this.
var newLocationsArray = locations.slice();
var newLocationNameArray = locationsNames.slice();

Why can I add, but not remove an element from a set

I’m trying to update the notification count in my database.
I’m doing this by creating a set, which I add a UID to when I want to add to the notification count and removes a UID from the set when I want to subtract from the notification count.
I then take the size of the set and update the notification count.
the updateNotificationCount function is triggered by a lower order component.
However I can only get the database to update when isNewMatch is true. Why won’t it update the database when isNewMatch is false?
state = {notificationSet: new Set()}
updateNotificationCount = (uid, isNewMatch) => {
if (isNewMatch) {
this.setState(({ notificationSet }) => ({
notificationSet: new Set(notificationSet).add(uid)
}));
}
else {
this.setState(({ notificationSet }) => {
const newNotificationSet = new Set(notificationSet);
newNotificationSet.delete(uid);
return {
notificationSet: newNotificationSet
};
});
};
}
You don't need to do new Set() every time because you already initialize the state with new Set() so now you just do as follow:
state = {notificationSet: new Set()}
updateNotificationCount = (uid, isNewMatch) => {
let notificationSet;
if (isNewMatch) {
notificationSet=this.state.notificationSet;
notificationSet.add(uid);
this.setState({
notificationSet: notificationSet
});
} else {
notificationSet=this.state.notificationSet;
notificationSet.delete(uid);
this.setState({
notificationSet : notificationSet
});
};
}

How to programmatically switch a switch in React Native?

I make us several Switch Components in one view. When I switch on one switch, I want all others to switch off. Currently, I set the boolean value property via the state. This results in changes happen abruptly because the switch is just re-rendered and not transitioned.
So how would you switch them programmatically?
EDIT 2: I just discovered that it runs smoothly on Android so it looks like an iOS-specific problem.
EDIT: part of the code
_onSwitch = (id, switched) => {
let newFilter = { status: null };
if (!switched) {
newFilter = { status: id };
}
this.props.changeFilter(newFilter); // calls the action creator
};
_renderItem = ({ item }) => {
const switched = this.props.currentFilter === item.id; // the state mapped to a prop
return (
<ListItem
switchButton
switched={switched}
onSwitch={() => this._onSwitch(item.id, switched)}
/>
);
};