I'm new to Vue and I'd like to make an AJAX call every time my component is rendered.
I have a vue component lets say "test-table" and Id like to fetch the contents via an AJAX call. There are many such tables and I track the active one via an v-if/v-else-if etc.
Currently I have a cheaty solution: in the template for the component I call a computed property called getData via {{ getData }} which initiates the Ajax call but does only return an empty string. Id like to switch to the proper way but dont know how.
My code is like so: (its typescript)
Vue.component("test-table", {
props: ["request"],
data () {
return {
tableData: [] as Array<TableClass>,
}
},
template: `{{ getData() }} DO SOME STUFF WITH tableData...`,
computed: {
getData() : string {
get("./foo.php", this.request, true).then(
data => this.tableData = data.map(element => new TableClass(data))
)
return "";
}
}
}
HTML:
<test-table v-if="testcounter === 1" :request="stuff...">
<test-table v-else-if="testcounter === 2" :request="other stuff...">
...
get is an async method that just sends a GET request with request data to the server. The last parameter is only for saying the method to expect a JSON as answer. Similar to JQuerys getJSON method.
the "created" method does NOT work! It fires only one time when the component is first created. If I deactivate and activate again (with v-if) the method is not called again.
Btw: I'm using Vue 2.6.13
Lifecycle hooks won't fire every time if the component is cached, keep-alive etc
Add a console.log in each of the lifecycle hooks to see.
Change to use a watcher which handles firing getData again if request changes.
...
watch: {
request: {
handler: function() {
this.getData()
},
deep: true
}
},
created() {
this.getData()
},
methods: {
getData(): string {
// do request
}
}
#FlorianBecker try the lifecycle hook updated(). It may be a better fit for what you're trying to achieve. Docs here.
You should be able to use the mounted hook if your component is continuously rendered/unrendered using v-if, like so:
export default {
mounted() {
// do ajax call here
this.callAMethod();
},
...
}
Alternatively, you could use the created() hook but it is executed earlier in the chain, so this means the DOM template is not created yet so you cant refer to it. mounted usually is the way to go.
More info on these hooks can be found here.
Related
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.
With the below code I would like to use react-select but when I console.log(testUsers) at first this is blank and then data is finally there, but in the select data is blank. Is there any way to not select blank?
My code:
const { request: getUser, isLoading } = useRequest("");
const [testUsers, setUsers] = useState("");
useEffect(() => {
getUser({
path: `${someapi}/user?id=${record?.user_uid}`,
overwritePath: true,
}).then((data: any) => {
setUsers(data[0].fullname);
});
}, [testUsers]);
console.log(testUsers, "/////////////////");
The output of the console:
/////////
////////
////////
some api returns /////////////
It's to know that React runs the callback of an useEffect after all others normal JavaScript codes such as a console.log() and after the JSX is rendered. And even if that wasn't the case, a network request is asynchronous so you get the data after some delay.
The workaround here is to use a conditional rendering. Something like this as an exmple:
{!testUsers ? <p>Loading...</p> : <div>Render actual content</div> }
But the main error you are making here is to add testUsers in the dependency array. Since you are calling a state setter that's muting it you would get an infinite calls. Do like this instead:
useEffect(() => {
getUser({
path: `${someapi}/user?id=${record?.user_uid}`,
overwritePath: true,
}).then((data: any) => {
setUsers(data[0].fullname);
});
}, []);
Lastly, about why you are getting multiple console.log(), you can check this thread to get a detailed answer.
The reason for this behavior is React Life Cycle. what happened is that in the Mounting phases.
constructor()
getDerivedStateFromProps()
render()
componentDidMount().
because render is triggering first you see that blank select in your DOM.
Read more about it: https://www.w3schools.com/react/react_lifecycle.asp.
Solution:
in your JSX you can check if you have a value in your state then render your JSX.
{testUsers && <h1>Show</h1>}
I want to set setInterval inside an onChange event, like mostly it is done on componentDidMount.
I have 2 dropdowns that filter data and then render a table, the dropdowns are controlled by onChange methods. The last on change method has a request that needs to be re-polled every X # seconds to get updated information from the server. However, these methods are outside of cDM so I'm not sure how to handle the setInterval like I previously have.
cDM() {
//axios call, update state with fetched data
}
onChange1 () {
// uses data from cDM to render the options in a dropdown.
// another axios call based on the selection, fetches data to render more options in subsequent dropdown
}
onChange2 () {
//dropdown 2 use data from onChange1 axios call. After the selection from the dropdown is made, it makes an api call that then renders data to a table.
//Some of this data updates every 5 seconds, so I need to re-poll this service to get updated information from the server.
}
if all the data was in cDM I'd normally change the data requests in cDM to an arrow function to avoid setState issues, call the function inside/outside of the setInterval callback with the following:
componentDidMount() {
this.interval = setInterval(() => this.getData(), 10000);
this.getData();
}
componentWillUnMount() {
clearInterval(this.interval);
}
getData = () => {
//data requests
}
SetInterval does not wait for the AJAX response before polling starts over again. This can become extremely buggy/memory intensive in the event of network problems.
I would suggest you use a setTimeOut and every update I would put a piece of response data in state and start the timer upon state changes inside your render function. That way you always ensure you get your result back before pounding the server again and bogging down your client's UI.
Place the code of componentDidMount body into both onChange events.
Set Interval equal to some state variable, so on componentWillUnmount you can access that interval and remove it.
onChange () {
// Change Logic
....
// Interval logic
clearInterval(this.state.interval); //Clear interval everytime.
this.state.interval = setInterval(() => this.getData(), 10000);
this.getData();
}
componentWillUnMount() {
clearInterval(this.state.interval);
}
You can indeed place this in your update event handler - event handlers do not have to be pure. There is the minor issue that doing this means that your view is no longer a function of state + props, but in reality that's not usually a problem.
It seems your main concern is how you model this data.
One way to do this might be to use a higher order component or some kind of composition, splitting the display of your component from the implementation of the business logic.
interface Props<T> {
intervalMilliseconds: number
render: React.ComponentType<ChildProps<T>>
}
interface State<T> {
// In this example I assume your select option will be some kind of string id, but you may wish to change this.
option: string | undefined
isFetching: boolean
data: T
}
interface ChildProps<T> extends State<T> {
setOption(option: string | undefined): void
}
class WithPolledData<T> extends React.Component<Props<T>, State<T>> {
interval: number
componentDidMount() {
this.setupSubscription()
}
componentDidUnmount() {
this.teardownSubscription()
}
setupSubscription() {
this.interval = setInterval(() => this.fetch(), this.props.intervalMilliseconds)
}
teardownSubscription() {
clearInterval(this.interval)
}
componentDidUpdate(previousProps: Props, previousState: State) {
if (previousProps.intervalMilliseconds !== this.props.intervalMilliseconds) {
this.teardownSubscription()
}
if (previousState.option !== this.state.option && this.state.option !== undefined) {
this.fetch()
}
}
fetch() {
if (this.props.option === undefined) {
// There is nothing to fetch
return
}
// TODO: Fetch the data from the server here with this.props.option as the id
}
setOption = (option: string | undefined) => {
this.setState({ option })
}
render() {
return React.createElement(
this.props.render,
{
...this.state,
setOption: this.setOption
}
)
}
}
You could use this component like so:
<WithPolledData intervalMilliseconds={5000} render={YourComponent} />
YourComponent would call setOption() whenever a drop down option was selected and the fetching would happen in the background.
This is how you would technically do this. I agree with #ChrisHawkes that using setInterval like this is likely a mistake - You would at the very least need to cancel in-flight HTTP requests and you run the risk of causing issues on devices with poor network performance.
Alternatively, a push rather than a pull model may be better here - This scenario is exactly what WebSockets are designed for and it wouldn't require too much modification to your code.
I got a problem here. I don't know how to call a method within mutation method, I've tried this.show() but it doesn't work, is there's a way like actions in vuex that you can call a method within the action using dispatch ?
// Mutation
export default {
show(state, payload) {
// execute code
},
hide(state, payload) {
// how to call show method?
}
}
I want mutation look like this
// Action
export default {
show({ state }, payload) {
// execute code
},
hide({ dispatch, state }, payload) {
// how to call show method look like this in mutation?
// Is this possible for mutation?
return dispatch('show', payload)
}
}
In VueJS 2 you should use $emit instead of $broadcast as described in documentation - https://v2.vuejs.org/v2/guide/migration.html#dispatch-and-broadcast-replaced
I have some components that should do some work as soon as their data has arrived and rendered for the first time, but not for future rerenderings. For example: Comments are loaded and rendered, now 1. load social media libraries and 2. load some Google Analytics.
Right now I'm doing it like that:
componentDidUpdate: function (prevProps, prevState) {
if (this.hasAlreadyUpdatedOnce) {
// ... do some stuff...
} else {
// ... do some stuff that should happen only once...
// 1. load social media libraries
// 2. load some Google Analytics
this.hasAlreadyUpdatedOnce = true;
}
}
But I'm asking myself if there's a more elegant way than setting a property like that.
Assuming you're responding to a state change, you should pass a callback as the second argument to setState.
componentDidMount: function(){
ajaxyThing(function(data){
this.setState({data: data}, function(){
// this.state is updated, the component has rerendered
// and the dom is current
});
}.bind(this));
}
You want componentDidMount(). Details here.
Have you tried updating state once the ajax call has finished?
Or you can return false for componentShouldUpdate and once the ajax call promise has resolved call forceUpdate.
I can't give you a definitive answer because I don't know if your ajax call is in the parent or child component but either way you should be able to leverage shouldComponentUpdate() to accomplish your goals. If you really don't ever want to update your component after the ajax call comes in then you can do something like this:
shouldComponentUpdate() {
return false;
}
and then when your ajax call comes back just run this.forceUpdate(). returning false will make it so that your component never updates unless you run this.forceUpdate(). However this is not the best solution to the problem I just can't give a better one without more information.
The React docs have a good example on how to handle this using isMounted().
isMounted() returns true if the component is rendered into the DOM,
false otherwise. You can use this method to guard asynchronous calls
to setState() or forceUpdate().
Example
First, initialize your state variables in `getInitialState()':
getInitialState: function() {
return {
username: '',
lastGistUrl: ''
}
}
In componentDidMount() make the ajax call ($.get in this case) then re-set the state variables:
componentDidMount: function() {
$.get(this.props.source, function(result) {
var lastGist = result[0];
if (this.isMounted()) {
this.setState({
username: lastGist.owner.login,
lastGistUrl: lastGist.html_url
});
}
}.bind(this));
}