How to handle vue composition api with lazy and route components? - javascript

In my vue application I have a component page that contains five components. I also use vue composition api.
each component is lazy, it resolve when the component is in the viewport. because of that I need to write the logic only inside the component. (not in the parent).
so far so good, but two of components inside product.vue depend of the route params value. say /product/2.
the product.vue looks like that:
<intro> -> just a raw text (no need to fetch from the server, not need to wait to load)
<foo> -> need data from the server base on the productId from the route.params.
<txt> -> just a raw text not need to get the data from the server
<bar> -> need data from the server base on the productId from the route.params.
Important notes:
In case I don't need data from the server like intro I want to render it immediately. so for all similar components in this page.
for example intro render right way, the foo will render when it enter to the viewport and display a loader and when the data is resolved it change to the data.
I want to see the page ASAP. so each component have its own loader.
Here I become with some problems:
One: I need to know if the product exist. if not it need to be redirect to 404 page. so where to put this logic? each component or the parent component? I think the parent component, but when it take 2 min to load the data I'll get a blank screen seince the components are not ready and not be render. which is conflict with "Important notes".
Two: when the productId is change (navigate to another product) the foo and bar need to resolve the data, so in those two components I need to listen to the route change (which makes duplication in my code) (and slow performance?) and still I need to check if the productId is exist or not.
in this example the items don't change because it need to trigger the useProduct function.
setup() {
const { route, router } = useRouter();
const productIdToLoad = computed(() => route.value.params.productId);
const { items } = useProduct({ productId: productIdToLoad });
return { items }
}
and if I do it in watch I lose the reference
setup() {
const { route, router } = useRouter();
const items = ref([]);
const productIdToLoad = computed(() => route.value.params.productId);
watch(productIdToLoad, (v) => {
items = useProduct({ productId: v });
});
return { items }
}

Related

Possible/How to use a VueJS component multiple times but only execute its created/mounted function once?

I am trying to create a VueJS component that does the following: 1) download some data (a list of options) upon mounted/created; 2) display the downloaded data in Multiselct; 3) send selected data back to parent when user is done with selection. Something like the following:
<template>
<div>
<multiselect v-model="value" :options="options"></multiselect>
</div>
</template>
<script>
import Multiselect from 'vue-multiselect'
export default {
components: { Multiselect },
mounted() {
this.getOptions();
},
methods:{
getOptions() {
// do ajax
// pass response to options
}
},
data () {
return {
value: null,
options: []
}
}
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
This is mostly straightforward if the component is only called once in a page. The problem is I may need to use this component multiple times in one page, sometimes probably 10s of times. I don't want the function to be called multiple times:
this.getOptions();
Is there a way to implement the component somehow so no matter how many times it is used in a page, the ajax call will only execute once?
Thanks in advance.
Update: I assume I can download the data in parent then pass it as prop if the component is going to be used multiple times, something like the following, but this defies the purpose of a component.
props: {
optionsPassedByParents: Array
},
mounted() {
if(this.optionsPassedByParents.length == 0)
this.getOptions();
else
this.options = this.optionsPassedByParents;
},
The simple answer to your question is: you need a single place in charge of getting the data. And that place can't be the component using the data, since you have multiple instances of it.
The simplest solution is to place the contents of getOptions() in App.vue's mounted() and provide the returned data to your component through any of these:
a state management plugin (vue team's recommendation: pinia)
props
provide/inject
a reactive object (export const store = reactive({/* data here */})) placed in its own file, imported (e.g: import { store } from 'path/to/store') in both App.vue (which would populate it when request returns) and multiselect component, which would read from it.
If you don't want to request the data unless one of the consumer components has been mounted, you should use a dedicated controller for this data. Typically, this controller is called a store (in fairness, it should be called storage):
multiselect calls an action on the store, requesting the data
the action only makes the request if the data is not present on the store's state (and if the store isn't currently loading the data)
additionally, the action might have a forceFetch param which allows re-fetching (even when the data is present in state)
Here's an example using pinia (the official state management solution for Vue). I strongly recommend going this route.
And here's an example using a reactive() object as store.
I know it's tempting to make your own store but, in my estimation, it's not worth it. You wouldn't consider writing your own Vue, would you?
const { createApp, reactive, onMounted, computed } = Vue;
const store = reactive({
posts: [],
isLoading: false,
fetch(forceFetch = false) {
if (forceFetch || !(store.posts.length || store.isLoading)) {
store.isLoading = true;
try {
fetch("https://jsonplaceholder.typicode.com/posts")
.then((r) => r.json())
.then((data) => (store.posts = data))
.then(() => (store.isLoading = false));
} catch (err) {
store.isLoading = false;
}
}
},
});
app = createApp();
app.component("Posts", {
setup() {
onMounted(() => store.fetch());
return {
posts: computed(() => store.posts),
};
},
template: `<div>Posts: {{ posts.length }}</div>`,
});
app.mount("#app");
<script src="https://unpkg.com/vue/dist/vue.global.prod.js"></script>
<div id="app">
<Posts v-for="n in 10" :key="n" />
</div>
As you can see in network tab, in both examples data is requested only once, although I'm mounting 10 instances of the component requesting the data. If you don't mount the component, the request is not made.

React/nextjs fetch data on a button click, but the button is in a different component

I have a function in the that fetches the weather data for current location when button is clicked. I want to get this data from the Location component to the pages/index.tsx where I have another components that displays that data.
This is my component structure:
App
--Layout
--Header
--Location (here i fetch the data on click)
--Home (state)
--Display 1
--SubDisplay 1
--Display 2
Do I have to pass the data through all the components to the index page? It would be:
UP from Location to Header
UP from Header to Layout
DOWN from Layout to Index page
DOWN from Index page to another components
Is this the best way to handle this?
Due to React's nature regarding data flow, you can only pass props down from parent component to its children, this is known as one-way data binding or unidirectional data flow. Based on the structure of your components, to achieve what you want to do you would need to use Context API or something like redux to centralize your data in one place (or just lifting the states up). However you can try this "workaround" so you don't need to modify your project's structure:
Since your state is in the index/home page, you could define a function in this component that will receive as parameter the data fetched in Location component and set the state with this data, of course you'd need to pass this function via props to Layout component so it can pass it to Header and Header to Location (prop drilling), example:
const Home = () => {
const [data, setData] = useState([]);
const handleData = (data) => {
setData(data);
}
return (
<Layout handleData={handleData}>
The rest of your components...
</Layout>
)
}
Then like I said, pass that function this way: Layout - Header - Location.
In Location:
const Location = ({ handleData }) => {
const handleClick = () => {
// Fetch your data on click event and pass it as parameter to handleData function
handleData(fetchedData);
}
// The rest of your component...
}
Hope this helps you.
Suggested solutions based on more complexity
Redux
React context API
Lifting State Up
Lifting state up (Easier than other) sample code
Define shipping function and weatherData as a state in Layout Component, then send shippingFunction into Header component then Location component as prop
const [weatherData,setWeatherData]=useState();
const shippingFunction = function(data) { setWeatherData(data);}
Location Component
Call props.shippingFunction in Layout component when weather
data was fetched, then send weather data into
props.shippingFunction as parameter. Now you have weather data
in Layout component
Layout Component
At this moment you have filled weatherData and you can pass it as prop into any component you want

React Query // How to properly use client-state and server-state (when user can drag/sort items on the UI)?

I have an React App using React Query to load a dataset of folders containing items. The app allows the user to drag/sort items (within a folder and drag to another folder). Before implementing drag/sort it was simple, RQ fetched the dataset folderData and then the parent supplied data to child components: parent > folder > item .
In order to implement drag/sort, I am now having to copy the entire dataset folderData into a client-state variable foldersLocal. This is the only way I could figure out how to change the UI when the user moves an item.
However, I feel like this approach essentially removes most of the benefits of using React Query because as soon as the data is fetched, I copy the entire dataset into a "client-side" state variable (foldersLocal) and only work with it. This also makes the QueryClientProvider functionality effectively useless.
Is there a better way to approach this? Or am I missing something?
// Fetch data using React Query
const {isLoading, isError, foldersData, error, status} = useQuery(['folders', id], () => fetchFoldersWithItems(id));
// Place to hold client-state so I can modify the UI when user starts to drag/sort items
const [foldersLocal, setFoldersLocal] = useState(null);
// Store React Query data in client-state everytime it's fetched
useEffect(() => {
if (status === 'success') {
setFoldersLocal(foldersData);
}
}, [foldersData]);
// Callback to change "foldersLocal" when user is dragging an item
const moveItem = useCallback((itemId, dragIndex, hoverIndex, targetFolderId) => {
setFoldersLocal((prevData) => {
// change data, etc. so the UI changes
}
}
// Drastically simplified render method
return (
<QueryClientProvider client={queryClient}>
<FolderContainer folders={foldersLocal} moveItem={moveItem} moveItemCompleted={moveItemCompleted}/>
</QueryClientProvider>
)

Next JS: Right way to fetch client-side only data in a Functional Component

I have a functional component. Basically, the page consists of a form - where I need to populate some existing data into the form and let the user update it.
I'm using a hook that I wrote to handle the forms. What it does is this
const [about, aboutInput] = useInput({
type: 'textarea',
name: 'About you',
placeholder: 'A few words about you',
initialValue: data && data.currentUser.about,
})
about is the value and aboutInput is the actual input element itself. I can pass an initialValue to it too.
At the beginning of the component I'm fetching the data like so:
const { data, loading, error } = useQuery(GET_CURRENT_USER_QUERY)
This only is executed on the client side and on the server side data is undefined.
Hence this code only works when I navigate to the page through a Link component from another client-side page.
It doesn't work for:
When I go the URL directly
When I navigate to this page from another SSR page(which uses getInitailProps)
I don't want to use lifecycle methods/class component(since I'm using hooks, and want to keep using the functional component.
Is there a nice way to achieve this in Next JS and keep using functional component?
You can fetch client-side only data using the useEffect Hook.
Import it first from react
import { useEffect } from 'react';
Usage in the component looks like follows
useEffect(() => {
return () => {
// clean-up functions
}
}, []);
The first argument Is a function and you can make your API calls inside this.
The second argument to the useEffect will determine when the useEffect should be triggered. If you pass an empty array [ ], then the useEffect will only be fired when the component Mounts. If you want the useEffect to fire if any props change then pass such props as a dependency to the array.
If you want GET_CURRENT_USER_QUERY from the query string you can pass the argument from the getInitailProps and read this as props in the useEffect array dependency.
I see you mentioned getInitailProps. So you probably know what it does and its caveats. If you want the data to be defined on server side, why not use getInitailProps on the page which the form is in. That way you can retrieve the data from props and pass it to your form component. It should work either directly visiting the url or visiting from your other pages.

Subscriptions with react-meteor-data

From the docs, I wrote my container like so
export default InventoryItemsList = createContainer(() => {
const itemsCollection = Meteor.subscribe('allInventoryItems');
const loading = !itemsCollection.ready();
return {
loading,
items: !loading ? InventoryItems.find().fetch() : []
};
}, class InventoryItemsListComponent extends Component {
render() {
let items = this.props.items;
/* some render logic */
return /*... react component template ...*/ ;
}
});
The problem I see is that
The container function is executed many times, thus calling Meteor.subscribe more than once; is that good? Will Meteor just ignore subsequent subscriptions?
According to this tutorial, subscriptions need to be stopped, but the docs do not mention it at all. This does not take care on it's own, does it?
What is the recommended way to stop (i.e. unsubscribe) or resolve the 2 issues that I see from that point?
Is TrackerRact actually better? (yes, I know this is opinionated, but surely there is some form of a convention with meteor react, here!)
1) The container component is a reactive component, so whenever documents are changed or added to the given collection, it's going to call via your container and update the DOM.
2) As far as I know, the container will only subscribe to the collection via the actual component you bind it to. Once you leave that component, the subscriptions should stop.
If you want to unsubscribe directly, you can just call this.props.items.stop() in your componentWillUnmount() method.
Finally, I will have to say that using React specific implementations are always better than using Meteor specific functions (i.e. it's always better to use state variables over Sessions with React, as it's always better to use containers than Tracker.autorun() with React, etc, etc).
About your problem 2), this is how I solve it
1- When you subscribe to something, store those subscriptions references and pass them to the component.
Here an example with 2 subscriptions, but subscribing to only 1, is even easier.
createContainer((props) =>{
const subscription = Meteor.subscribe('Publication1', props.param1);
const subscription2 = Meteor.subscribe('Publication2', props.param1, props.param2);
const loading = !(subscription.ready() && subscription2.ready());
const results1 = loading ? undefined : Collection1.find().fetch();
const results2 = loading ? undefined : Collection2.findOne({a:1});
return {subscriptions: [subscription, subscription2], loading, results1, results2};
}, MyComp);
Then in my component:
class MyComp extends Component {
......
componentWillUnmount() {
this.props.subscriptions.forEach((s) =>{
s.stop();
});
}
....
}
This way, the component will get in props.subscriptions all the subscriptions it needs to stop before unmounting.
Also you can use this.props.loading to know if the subscriptions are ready (of course, you can have 2 different ready1 and ready2 if it helps).
Last thing, after subscribing, if you .find(), dont forget to .fetch(), else the results will not be reactive.

Categories

Resources