Using 1 Axios call for multiple components - javascript

I am running a simple Axios call like so:
.get('https://myAPI.com/')
.then(response => {
this.info = response.data
})
And then display the data through a v-for array loop on my components. The problem is that I am running this mounted Axios call on each component I use it for. For example, I have a component for desktop screens that uses this axios call to display data in sidebar, while my mobile screen component uses the exact same axios call too display in a header.
The problem is that I am running multiple calls to the same API since each component is using the mounted axiox function.
Is there a way to run this call once and then utilize the v-for loop on each component?

Use Vuex for such task.
I'll make a very simple example.
Install vuex and axios in your project
later create a file in your project call, store.js.
store.js
import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";
const store = new Vuex.Store({
state: {
info : []
},
mutations: {
updateInfo (state, info) {
state.info = info
}
},
actions: {
fetchData({commit}) {
axios.get('https://myAPI.com/')
.then(response => {
commit('updateInfo', response.data )
})
}
}
})
in your main.js import store.js file
import store from "./store";
new Vue({
...
store,
...
});
in your App.vue dispatch 'updateInfo' action.
App.vue
...
created() {
this.$store.dispatch("fetchData");
}
...
And in the component you want to use the info data component, set:
...
computed: {
info() {
return this.$store.state.info
}
},
...
and use info to render the elements with the v-for directive.
This info refers the array of elements you bring

OK, I've found a way to handle this without Vuex. My example: I have two components TrainingCourseComponent and CertificateComponent.
In TrainingCourseComponent:
data() {
return {
trainings : {},
},
methods:{
loadTrainingCenters(){
axios.get("/trainingCourse")
.then(({data}) => {
this.trainings = data;
Event.$emit('globalVariables', data);
});
}
}
created(){
this.loadTrainingCenters();
}
and you can do this in any other component but in this case CertificateComponent(you can define it in mounted() or created() method it doesn't matter:
data() {
return {
training_courses:{}
}
}
mounted(){
Event.$on('globalVariables', (trainings) => {
this.training_courses = trainings;
});
}
p.s. I guess you know but just in case Event is a global Vue instance defined in app.js that I use for different kind of stuff :)
app.js
/**
* some custom event
*/
window.Event = new Vue();

Related

Vue asnyc await over several layers

I am currently building a small Vue JS app. I'm using a Simple State Management with Reactivity API to cache data between pages. But I also have some functions built in. One function is to fetch data from the backend. For the API communication I created a helper class that talks to the API. For example, a product list should be displayed. The component has a method that calls a store.js method. The store.js method then calls a method from the helper class. I work with fetch and async await. To display a list in the component view at the end of the day I need to pass the data from the helper class to the component using asnyc await. Now my question. Is this approach a crime against javascript? Is there a better solution for this?
The three relevant files.
// PageComponent
<script>
import { store } from '../helpers/store'
export default {
data() {
return {
store,
list: [],
}
},
methods: {
getData() {
store.getData()
}
},
}
</script>
// store.js
import { reactive } from 'vue'
import { comhelper } from "./comhelper";
export const store = reactive({
brand: "",
async getData() {
return await comhelper.getData();
}
});
// comhelper.js
export const comhelper = {
async getSurveyData() {
const d = await fetch('/data/db.json');
return await d.json();
}
}

Vuex 4, State is empty in component

I am trying to access subjects store state using this.$store.state.subjects inside my home component however it comes up as an empty array. Using console.log the only place I am able to see the state.subjects populated is if its in the mutation function.
Anywhere else the console.log is empty. It seems to me that the state is not persisting from the mutations, but I'm not sure why.
I have tried quite a few stackoverflow answers however, non of them fix the issues or I have no clue what I am reading in the post. I have also left of code from my code blocks to make this post more readable, such as imports or templates.
Store index.js
export default createStore({
state: {
subjects: [],
},
actions: {
getSubjects({ commit }) {
// Manages subjects, allow for display in column or Calendar view
axiosMain({
method: "get",
url: "/study/",
withCredentials: true,
})
.then((response) => {
commit("setSubjects", response.data);
})
},
},
mutations: {
setSubjects(state, subjectsToSet) {
state.subjects = subjectsToSet;
console.log(state.subjects) # Is a populated array
}
}
});
Main.js
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import VueGtag from "vue-gtag-next";
import store from "./store";
import "./assets/main.css";
createApp(App)
.use(router)
.use(store)
.use(VueGtag, {
property: {
id: "G-E4DPXQ96HB",
},
})
.mount("#app");
Home.vue
<template>
</template>
<script>
export default {
name: "Home",
data() {
return {
subjects: [],
};
},
mounted() {
this.callStoreSubjectAction();
this.setSubjectsToStoreSubject();
},
methods: {
callStoreSubjectAction() {
this.$store.dispatch("getSubjects");
},
setSubjectsToStoreSubject() {
this.subjects = this.$store.state.subjects;
console.log(this.$store.state.subjects); # Is an empty array
},
},
};
</script>
In the component, you're copying the value of this.$store.state.subjects before the axios call has completed. Wait for the promise to resolve first. To do that, you'll need to first return the promise from the action:
getSubjects({ commit }) {
return axiosMain({ // returning the promise
...
}
}
Waiting for the promise:
mounted() {
this.$store.dispatch("getSubjects").then(r => {
this.subjects = this.$store.state.subjects;
console.log(this.$store.state.subjects);
});
},
Better than this would be to remove subjects from your component data and use a computed instead to sync with the Vuex state:
import { mapState } from 'vuex';
computed: {
...mapState(['subjects']) // creates a computed `this.subjects`
}
Then you would only have to dispatch the action and the component will take care of the rest:
mounted() {
this.$store.dispatch("getSubjects");
}

Vue + Vuex using axios asynchrnously yet getters return empty array

My issue is getters that are returning initial state ([]).
in my component I have a created method that sets the axios call result into state.
created() {this.$store.dispatch("SET_STORIES");},
I have mapGetters in computed:
computed: {
...mapGetters(["GET_STORIES"])
},
And a method to get state:
methods: {
stories() {
return this.$store.getters.GET_STORIES;
}
}
mounted() is returning an empty array:
mounted() {
console.log("stories", this.$store.getters.GET_STORIES);
},
store.js
import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";
import VueAxios from "vue-axios";
import chunk from "lodash/chunk";
Vue.use(Vuex, VueAxios, axios);
export default new Vuex.Store({
state: {
stories: [],
twoChunkStories: []
},
getters: {
GET_STORIES: state => {
return state.stories;
}
},
mutations: {
SET_STORIES(state, stories) {
state.stories = stories;
},
SET_CHUNKED_STORIES(state, stories) {
state.twoChunkStories= stories;
},
},
actions: {
SET_STORIES: async ({ commit }) => {
const options = {
headers: {
"Content-Type": "application/json"
}
};
let { data } = await axios.get(
"https://api.example.com/get.json",
options
);
if (data.meta.code === 200) {
let storiesArray = data.data.stories;
let chunkSize = 2;
commit("SET_STORIES", storiesArray);
let chunkedArray = chunk(storiesArray, chunkSize);
commit("SET_CHUNKED_STORIES", chunkedArray);
}
}
}
});
How can I make an axios async call that would have state set onload on the earliest lifecycle hook (I thought created() was the earliest hook) and be ready to be called out on mounted. I'm obviously doing something wrong asynchronously over the getters, I just don't know exactly what.
You didn't call your action method of SET_STORIES in your component, so the stories in your store will not get updated, first you need to call the action from your Vue component like
mounted() {
this.$store.actions.SET_STORIES
}
Also, I think you need to use a different logic here, because you don't know how long does it take to fetch the stories data from your server.
In your component you can create a variable named isDataLoaded and make it false initially.
In your component you can conditionally render your list like
<div v-if="!isDataLoaded">
Loading ...
</div>
<div v-if="isDataLoaded">
... your list goes here ...
</div>
In your mounted() method you need to update isDataLoaded after the action call like this so your list will show up in the screen
async mounted() {
await this.$store.actions.SET_STORIES
this.isDataLoaded = true
}

Confused with REDUX actions and reducers

so I am trying to refactor some code from my previous question:
React: How to update one component, when something happens on another component
So I started digging deep into the existing code template to see how it was implemented.
I found a reducers.js where I added a new reducer: ActiveTenant
import Auth from './auth/reducer';
import App from './app/reducer';
import ThemeSwitcher from './themeSwitcher/reducer';
import LanguageSwitcher from './languageSwitcher/reducer';
import ActiveTenant from './activetenant/reducer';
export default {
Auth,
App,
LanguageSwitcher,
ThemeSwitcher,
ActiveTenant
};
That new reducer is like this:
import { Map } from 'immutable';
import actions from './actions';
import { adalApiFetch } from '../../adalConfig';
const initState = new Map({
tenantId: ''
});
export default function(state = initState, action) {
switch (action.type) {
case actions.SET_TENANT_ACTIVE:
{
const options = {
method: 'post'
};
adalApiFetch(fetch, "/Tenant/SetTenantActive?TenantName="+state.tenantId, options)
.then(response =>{
if(response.status === 200){
console.log("Tenant activated");
}else{
throw "error";
}
})
.catch(error => {
console.error(error);
});
return state.set('tenant', state.Name);
}
default:
return state;
}
}
and actions for that reducer
const actions = {
SET_TENANT_ACTIVE: 'SET_TENANT_ACTIVE',
setTenantActive: () => ({
type: actions.SET_TENANT_ACTIVE
}),
};
export default actions;
Then from the component itself, I need to call the action when a row is selected on the front end, so I have refactored the commented code, into one line.
import React, { Component } from 'react';
import { Table, Radio} from 'antd';
import { adalApiFetch } from '../../adalConfig';
import Notification from '../../components/notification';
import actions from '../../redux/activetenant/actions';
const { setTenantActive } = actions;
class ListTenants extends Component {
constructor(props) {
super(props);
this.state = {
data: []
};
}
fetchData = () => {
adalApiFetch(fetch, "/Tenant", {})
.then(response => response.json())
.then(responseJson => {
if (!this.isCancelled) {
const results= responseJson.map(row => ({
key: row.id,
TestSiteCollectionUrl: row.TestSiteCollectionUrl,
TenantName: row.TenantName,
Email: row.Email
}))
this.setState({ data: results });
}
})
.catch(error => {
console.error(error);
});
};
componentDidMount(){
this.fetchData();
}
render() {
const columns = [
{
title: 'TenantName',
dataIndex: 'TenantName',
key: 'TenantName',
},
{
title: 'TestSiteCollectionUrl',
dataIndex: 'TestSiteCollectionUrl',
key: 'TestSiteCollectionUrl',
},
{
title: 'Email',
dataIndex: 'Email',
key: 'Email',
}
];
// rowSelection object indicates the need for row selection
const rowSelection = {
onChange: (selectedRowKeys, selectedRows) => {
if(selectedRows[0].TenantName != undefined){
console.log(selectedRows[0].TenantName);
const options = {
method: 'post'
};
setTenantActive(selectedRows[0].TenantName);
/* adalApiFetch(fetch, "/Tenant/SetTenantActive?TenantName="+selectedRows[0].TenantName.toString(), options)
.then(response =>{
if(response.status === 200){
Notification(
'success',
'Tenant set to active',
''
);
}else{
throw "error";
}
})
.catch(error => {
Notification(
'error',
'Tenant not activated',
error
);
console.error(error);
}); */
}
},
getCheckboxProps: record => ({
type: Radio
}),
};
return (
<Table rowSelection={rowSelection} columns={columns} dataSource={this.state.data} />
);
}
}
export default ListTenants;
However, its not clear to me the relationship between the action and the reducer, if I check the debugger the action is executed, and none parameter is received, but the reducer is never executed.
DO i have to put a dispatch somewhere?, what I am missing in this puzzle?
So the first thing to understand is the Redux Cycle:
Action Creator-->Action-->dispatch-->Reducers-->State
Action Creator: An action creator is a function that is going to create or return a plain JavaScript object knowns as an Action with a type property and payload property which describes some change you want to make on your data.
The payload property describes some context around the change we want to make.
The purpose of an Action is to describe some change to the data inside our application.
The Action Creator is to create the Action.
The dispatch function is going to take in an Action and make copies of that object and pass it off to a bunch of different places inside our application which leads us to the Reducers.
In Redux, a reducer is a function responsible for taking in an Action. Its going to process that Action, make some change to the data and return it so it can be centralized in some location.
In Redux, the State property is a central repository of all information produced by our reducers. All the information gets consolidated inside the State object so our React application can easily reach into our Redux side of the app and get access to all the data inside the application.
So this way the app does not have to go around to each separate reducer and ask for the current State.
So digest that for a couple of minutes and then look at your architecture.
Let's skip over to reducers.
Reducers are called with an Action that was created by an Action Creator. The reducer will take a look at that Action and decide whether it needs to modify some data based on that Action.
So in other words, the job of a reducer is not to execute API requests but to process actions sent to it by the action creator.
So instead of this:
import { Map } from 'immutable';
import actions from './actions';
import { adalApiFetch } from '../../adalConfig';
const initState = new Map({
tenantId: ''
});
export default function(state = initState, action) {
switch (action.type) {
case actions.SET_TENANT_ACTIVE:
{
const options = {
method: 'post'
};
adalApiFetch(fetch, "/Tenant/SetTenantActive?TenantName="+state.tenantId, options)
.then(response =>{
if(response.status === 200){
console.log("Tenant activated");
}else{
throw "error";
}
})
.catch(error => {
console.error(error);
});
return state.set('tenant', state.Name);
}
default:
return state;
}
}
Your reducer should look something like this:
import { SET_TENANT_ACTIVE } from "../actions/types";
const initialState = {
tenantId: ''
};
export default (state = initialState, action) {
switch (action.type) {
case SET_TENANT_ACTIVE:
return {...state, [action.payload.id]: action.payload };
default:
return state;
}
}
Then inside your action creators file, you should have an action creator that looks something like this:
import axios from 'axios';
import { SET_TENANT_ACTIVE } from "../actions/types";
export const setTenant = id => async (dispatch) => {
const response = await axios.post(`/tenants/${id}`);
dispatch({ type: SET_TENANT_ACTIVE, payload: response.data });
};
You also need to learn about Redux project structure because after the above refactor, you are missing how to wire all this up to your component. In your component file there is no connect() function which also requires the Provider tag and you have none of that.
So for this I recommend first of all your set up your folder and file structure like so:
/src
/actions
/components
/reducers
index.js
So inside your index.js file it should look something like this:
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore, applyMiddleware, compose } from "redux";
import reduxThunk from "redux-thunk";
import App from "./components/App";
import reducers from "./reducers";
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducers,
composeEnhancers(applyMiddleware(reduxThunk))
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector("#root")
So your goal here is to ensure that you get that Provider tag at the very top of your component hierarchy and ensure that you pass it a reference to your Redux store that gets all the reducers loaded up into it.
So above I have created the store and passed it our set of reducers and it will return back to you all your applications State.
Lastly, what you see above is I created an instance of <Provider> and wrapped the <App /> component with it and then you want to pass the <Provider> component is a single prop called store. The store is the result of calling createStore() and calling the reducers.
The <Provider> is what interacts with the Redux store on our behalf.
Notice, I also have wired up Redux-Thunk that J. Hesters mentioned, you are making an ajax request as far as I can see from your code which is why I offered an asynchronous action creator for you, which means you will need Redux-Thunk or some middleware like that, let me not offend the Redux-Saga fans, so you have those two choice at least. You seem relatively new to Redux, just go with Redux-Thunk.
Now you can use the connect() component inside your component file to finish wiring up those action creators and reducers to your component or your React side of the application.
import React, { Component } from 'react';
import { connect } from "react-redux";
import { Table, Radio} from 'antd';
import { adalApiFetch } from '../../adalConfig';
import Notification from '../../components/notification';
import actions from '../../redux/activetenant/actions';
After importing connect, you create an instance of it below:
export default connect()(ListTenants);
Please don't argue with me on the above syntax (actually had a former student report me to administrators for using this syntax as evidence of not knowing what I was doing).
Then you need to configure this connect() React component by adding mapStateToProps if you are going to need it, but definitely pass in actions as the second argument to connect(). If you realize you don't need mapStateToProps, then just pass in null as the first argument, but you can't leave it empty.
Hope all this was helpful and welcome to the wonderful world of React-Redux.
You are using reducers wrong. Reducers are supposed to be pure. Yours has side-effects showing that you haven't understood Redux, yet.
Instead of writing down a solution for you (which would take forever anyways since one would have to explain Redux in total), I suggest you invest the 3 hours and go through the Redux docs and follow the tutorials (they are great).
Afterwards you might want to look into Redux Thunk. But, you might not need thunks.
PS: (Small thing to bring up, but I haven't seen anyone using Maps in Redux. Is there a reason you do that? You might want to use plain objects instead.)
Your action is not correct you should pass an active tenant name as parameter.
Ref. https://redux-starter-kit.js.org/api/createaction
We could have written the action types as inline strings in both places.
The action creators are good, but they're not required to use Redux - a component could skip supplying a mapDispatch argument to connect, and just call this.props.dispatch({type : "CREATE_POST", payload : {id : 123, title : "Hello World"}}) itself.
Ref. https://redux-starter-kit.js.org/usage/usage-guide

Property or method not defined on the instance but referenced during render I Vuex

I'm getting the following error.
[Vue warn]: Property or method "updateData" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
As far I can tell by the code, the method is there, so I'm stuck on something that I miss due to my ignorance of Vuex. I've googled the matter and got quite a few answers but none of them made me any wiser what to do. It seems to be something with scope, I'm sensing.
I also get the error below but I suspect that it's the same root cause for both so solving the one will resolve the other.
[Vue warn]: Invalid handler for event "click": got undefined
(found in component at ...)
The markup is as follow. I've checked that the path goes to the right location. At the moment I'm not sure at all how to even start to troubleshoot it. Any hints would be appreciated.
<template>
<div id="nav-bar">
<ul>
<li #click="updateData">Update</li>
<li #click="resetData">Reset</li>
</ul>
</div>
</template>
<script>
import { updateData, resetData } from "../vuex_app/actions";
export default {
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData, resetData }
}
}
</script>
Edit
After input I improved the export to include methods property like so. (Still the same error remaining, though.)
export default {
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData, resetData },
methods:{
updateData: () => this.$store.dispatch("updateData"),
resetData: () => this.$store.dispatch("resetData")
}
}
}
Do I have to do something extra in the store? It looks like this.
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const state = { dataRows: [], activeDataRow: {} };
const mutations = {
UPDATE_DATA(state, data) { state.dataRows = data; state.activeDataRow = {}; },
RESET_DATA(state) { state.dataRows = []; state.activeDataRow = {}; }
};
export default new Vuex.Store({ state, mutations });
You have to add the imported functions in the methods of Vue component, like following. You can take help of mapActions as explained in the documentation. This is needed to map this.updateDate to this.$store.dispatch('updateDate').
<script>
import { updateData, resetData } from "../vuex_app/actions";
import { mapActions } from 'vuex'
export default {
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData, resetData }
},
methods: {
...mapActions(['updateData', 'resetData'])
}
}
</script>
Edited
In case you dont want to use mapActions, you can use this.$store.dispatch as you are using in your example, however you need to have methods at vue compoenent level (documentation) and not insise vuex, as following:
export default {
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData, resetData }
},
methods:{
updateData: () => this.$store.dispatch("updateData"),
resetData: () => this.$store.dispatch("resetData")
}
}

Categories