Managing two states in one action in Vuex - javascript

I have two APIs reporting two sets of data (lockboxes and workstations). The lockboxes API has a collection of agencies with a recordId that I need to manipulate. The workstations API is the main collection that will assign one of these agencies (lockboxes) on a toggle to a workstation by sending the lockboxes.recordId and the workstation.recordId in the body to the backend.
My store looks like this
import { axiosInstance } from "boot/axios";
export default {
state: {
lockboxes: [],
workstation: []
},
getters: {
allLockboxes: state => {
return state.lockboxes;
},
singleWorkstation: state => {
let result = {
...state.workstation,
...state.lockboxes
};
return result;
}
},
actions: {
async fetchLockboxes({ commit }) {
const response = await axiosInstance.get("agency/subagency");
commit("setLockboxes", response.data.data);
},
updateAgency: ({ commit, state }, { workstation, lockboxes }) => {
const postdata = {
recordId: state.workstation.recordId,
agency: state.lockboxes.recordId
};
return new Promise((resolve, reject) => {
axiosInstance
.post("Workstation/update", postdata)
.then(({ data, status }) => {
if (status === 200) {
resolve(true);
commit("setWorkstation", data.data);
commit("assignAgency", workstation);
console.log(state);
}
})
.catch(({ error }) => {
reject(error);
});
});
}
},
mutations: {
setWorkstation: (state, workstation) => (state.workstation = workstation),
assignAgency(workstation) { workstation.assign = !workstation.assign},
setLockboxes: (state, lockboxes) => (state.lockboxes = lockboxes)
}
};
Process:
When I select a lockbox from the dropdown and select a toggle switch in the workstation that I want to assign the lockbox too, I do get the lockbox to show but it goes away on refresh because the change only happened on the front end. I'm not really passing the workstation.recordId or lockboxes.recordId in my body as I hoped I was. It is not reading the state and recognizing the recordId for either state(workstation or lockboxes).
the console.log is returning (Uncaught (in promise) undefined)
The request is 404ing with an empty Payload in the body ( {} )
Not even the mutation is firing
template
toggleAssign(workstation) {
this.updateAgency(workstation);
}
At some point I had it that is was reading the workstation.recordId before I tried to merge the two states in the getter but I was never able to access the lockboxes.recordId. How can I have access to two states that live in two independent APIs so I can pass those values in the body of the request?

You can add debugger; in your code instead of console.log to create a breakpoint, and inspect everything in your browser's debug tools.
I can't really help because there are very confusing things:
state: {
lockboxes: [],
workstation: []
},
So both are arrays.
But then:
setWorkstation: (state, workstation) => (state.workstation = workstation),
assignAgency(workstation) { workstation.assign = !workstation.assign},
It seems that workstation is not an array?
And also this, in the getters:
singleWorkstation: state => {
let result = {
...state.workstation,
...state.lockboxes
};
return result;
}
I'm not understanding this. You're creating an object by ...ing arrays? Maybe you meant to do something like:
singleWorkstation: state => {
let result = {
...state.workstation,
lockboxes: [...state.lockboxes]
};
return result;
}
Unless lockboxes is not an array? But it's named like an array, it's declared as an array. You do have this however:
const postdata = {
recordId: state.workstation.recordId,
agency: state.lockboxes.recordId
};
So it seems it's not an array?
Finally, in your updageAgency method, and this is where the problem may lie:
return new Promise((resolve, reject) => {
axiosInstance
.post("Workstation/update", postdata)
.then(({ data, status }) => {
if (status === 200) {
resolve(true);
commit("setWorkstation", data.data);
commit("assignAgency", workstation);
console.log(state);
}
})
.catch(({ error }) => {
reject(error);
});
});
The .then first arg of axios is only invoked if the status code is 2xx or 3xx. So your test if (status === 200) is superfluous because errors would not get there. And if for a reason of another you have a valid code other than 200, the promise never ends. reject is never called, as it's not an error, and neither is resolve. So you should remove check on the status code.
You should also call resolve(true) after the two commits, not before...
Finally your mutation assignAgency is declared all wrong:
assignAgency(workstation) { workstation.assign = !workstation.assign},
A mutation always takes the state as the first param. So it should either be:
assignAgency(state, workstation) {
state.workstation = {...workstation, assign: !workstation.assign}
},
Or
assignAgency(state) {
state.workstation = {...state.workstation, assign: !state.workstation.assign}
},
Depending on if you even need the workstation argument, given that what you want is just toggle a boolean inside an object.
TLDR: I'm not sure if lockboxes should be an array or an object, remove the status check inside your axios callback, fix the assignAgency mutation, use breakpoints with debugger; and the VueJS chrome plugin to help examine your store during development.

In an action, you get passed 2 objects
async myAction(store, payload)
the store object is the whole vuex store as it is right now. So where you are getting commit, you can get the state like so
async fetchLockboxes({ commit,state }) {//...}
Then you can access all state in the app.

You may use rootState to get/set whole state.
updateAgency: ({ commit, rootState , state }, { workstation, lockboxes }) {
rootState.lockboxes=[anything you can set ]
}

Related

Storing data from Axios Promise for multiple uses

I have created an endpoint in express that handles get requests. From a react component, I make a get request to said endpoint using axios. I want to store the data in an object in my Component class so that it can be accessed at multiple times (onComponentDidLoad, multiple onClick event handlers, etc). Is there a way to store the data outside of the axios promise, and/or preserve the promise so that I can do multiple .then calls without the promise being fulfilled?
I have tried using setState(), returning the promise, and returning the actual data from the get request.
Here is what I have right now:
constructor {
super();
this.myData = [];
this.getData = this.getData.bind(this);
this.storeData = this.storeData.bind(this);
this.showData = this.showData.bind(this);
}
// Store data
storeData = (data) => {this.myData.push(data)};
// Get data from API
getData() {
axios
.get('/someEndpoint')
.then(response => {
let body = response['data'];
if(body) {
this.storeData(body);
}
})
.catch(function(error) {
console.log(error);
});
}
showData() {
console.log(this.myData.length); // Always results in '0'
}
componentDidMount = () => {
this.getData(); // Get data
this.showData(); // Show Data
}
render() {
return(
<Button onClick={this.showData}> Show Data </Button>
);
}
Edit
I was incorrect in my question, storing the promise and then making multiple .then calls works. I had it formatted wrong when i tried it.
This code won't quite work because you're attempting to show the data without waiting it to be resolved:
componentDidMount = () => {
this.getData();
this.showData();
}
As you hinted toward in your original post, you'll need to extract the data from the Promise and there's no way to do that in a synchronous manner. The first thing you can do is simply store the original Promise and access it when required - Promises can be then()ed multiple times:
class C extends React.Component {
state = {
promise: Promise.reject("not yet ready")
};
showData = async () => {
// You can now re-use this.state.promise.
// The caveat here is that you might potentially wait forever for a promise to resolve.
console.log(await this.state.promise);
}
componentDidMount() {
const t = fetchData();
this.setState({ promise: t });
// Take care not to re-assign here this.state.promise here, as otherwise
// subsequent calls to t.then() will have the return value of showData() (undefined)
// instead of the data you want.
t.then(() => this.showData());
}
render() {
const handleClick = () => {
this.showData();
};
return <button onClick={handleClick}>Click Me</button>;
}
}
Another approach would be to try to keep your component as synchronous as possible by limiting the asyncrony entirely to the fetchData() function, which may make your component a little easier to reason about:
class C extends React.Component {
state = {
status: "pending",
data: undefined
};
async fetchData(abortSignal) {
this.setState({ status: "pending" });
try {
const response = await fetch(..., { signal: abortSignal });
const data = await response.json();
this.setState({ data: data, status: "ok" });
} catch (err) {
this.setState({ error: err, status: "error" });
} finally {
this.setState({ status: "pending" });
}
}
showData() {
// Note how we now do not need to pollute showData() with asyncrony
switch (this.state.status) {
case "pending":
...
case "ok":
console.log(this.state.data);
case "error":
...
}
}
componentDidMount() {
// Using an instance property is analogous to using a ref in React Hooks.
// We don't want this to be state because we don't want the component to update when the abort controller changes.
this.abortCtrl = new AbortController();
this.fetchData(this.abortCtrl.signal);
}
componentDidUnmount() {
this.abortCtrl.abort();
}
render() {
return <button onClick={() => this.showData()}>Click Me</button>
}
}
If you just store the promise locally and access it as a promise it should work fine.
getData() {
// if request has already been made then just return the previous request.
this.data = this.data || axios.get(url)
.then( response => response.data)
.catch(console.log)
return this.data
}
showData() {
this.getData().then(d => console.log('my data is', data));
}

Proper way to handle a page refresh based on a redux request change

I have created a redux that is going to request an API and if the result is 200, I want to redirect the user to another page using history.
The problem is: I don't know how to trigger this change if the action is a success.
I could redirect the user in my useCase function but I can't use history.push pathName/state argument because it only works in a React component.
So this is what I have done in my React component:
const acceptProposalHandler = () => {
store.dispatch(acceptProposal(id)).then(() => {
setTimeout(() => {
if (isAccepted) { //isAccepted is false by default but is changed to true if the
//request is 200
history.push({
pathname: urls.proposal,
state: {
starterTab: formatMessage({id: 'proposalList.tabs.negotiation'}),
},
});
}
}, 3000);
});
};
Sometimes it works but other times it wont. For some reason, .then is called even if the request fails.
I'm using setTimeOut because if I don't, it will just skip the if statement because the redux hasn't updated the state with isAccepted yet.
This is my useCase function from redux:
export const acceptProposal = (id: string) => async (
dispatch: Dispatch<any>,
getState: () => RootState,
) => {
const {auth} = getState();
const data = {
proposalId: id,
};
dispatch(actions.acceptProposal());
try {
await API.put(`/propostas/change-proposal-status/`, data, {
headers: {
version: 'v1',
'Content-Type': 'application/json',
},
});
dispatch(actions.acceptProposalSuccess());
} catch (error) {
dispatch(actions.acceptProposalFailed(error));
}
};
What I'm doing wrong? I'm using Redux with thunk but I'm not familiar with it.
".then is called even if the request fails." <- this is because acceptProposal is catching the API error and not re-throwing it. If an async function does not throw an error, it will resolve (i.e. call the .then). It can re-throw the error so callers will see an error:
export const acceptProposal = (id: string) => async (
// ... other code hidden
} catch (error) {
dispatch(actions.acceptProposalFailed(error));
// ADD: re-throw the error so the caller can use `.catch` or `try/catch`
throw error;
}
};

Vue.js with vuex and axios - can get data only on second load

I created a Vue.js app with a central store with vuex and some basic API calls with axios to fetch data into the store.
I create the following store action:
loadConstituencyByAreaCodeAndParliament({commit}, {parliament_id, area_code}) {
axios.get('/cc-api/area-code/' + parliament_id + '/' + area_code)
.then((response) => {
commit('SET_CONSTITUENCY', response.data);
})
.catch(function(error){
commit('SET_CONSTITUENCY', null);
}
)
}
In a single component file I defined a form where the user enters the area code. This form then calls this action to get the constituency fitting the area code:
export default {
name: 'AreaCodeForm',
components: {
PostalCodeInput
},
props: ['parliament_id'],
data: () => ({
postalCode: ''
}),
methods: {
search_area_code(submitEvent) {
let area_code = submitEvent.target.elements.area_code.value;
let payload = {
parliament_id: this.parliament_id,
area_code
}
this.$store.dispatch('loadConstituencyByAreaCodeAndParliament', payload).
then(() => {
let constituency = this.$store.getters.getConstituency();
// do some things with the data received from the API
// but everything depending on constituency does not work the first time.
// Data received from the API is here available only from the second time on
// wehen this code run.
})
}
}
}
As I found out the $store.dispatch method returns a promise but still the constituency variable receives not the data fetched with the loadConstituencyByAreaCodeAndParliament action but remains empty. I thought when I use the promise.then method the data should be already stored in the store but it is not. When I enter the area code a second time everything works well.
As mentioned by blex in a comment returning the axios call is the answer:
loadConstituencyByAreaCodeAndParliament({commit}, {parliament_id, area_code}) {
return axios.get('/cc-api/area-code/' + parliament_id + '/' + area_code)
.then((response) => {
commit('SET_CONSTITUENCY', response.data);
})
.catch(function(error){
commit('SET_CONSTITUENCY', null);
}
)
}
Always remember the return statement when dealing with asyncronous tasks.
You have two options to refactorize your code, keeping promise or async/await.
Option 1: async/await
async loadConstituencyByAreaCodeAndParliament({ commit }, { parliament_id, area_code }) {
try {
const { data } = await axios('/cc-api/area-code/' + parliament_id + '/' + area_code)
commit('SET_CONSTITUENCY', data)
return data
} catch (error) {
commit('SET_CONSTITUENCY', null)
return error
}
}
Notes:
return statement in both blocks of try/catch.
.get in axios is optional, since default is get method.
You can use object Destructuring assignment with { data } by default with axios. If I'm not wrong the default good http responses retrieve data.
Even a more sophisticated way could be const { data: constituencyResponse } = await... then you work with constituencyResponse and you probably save 2 or 3 lines of code each time.
Option 2: Promise
First Path: Make everything in the store.
// actions
loadConstituencyByAreaCodeAndParliament({ commit, dispatch }, { parliament_id, area_code }) {
axios('/cc-api/area-code/' + parliament_id + '/' + area_code)
.then(({data}) => {
commit('SET_CONSTITUENCY', data)
dispatch('actionTwo', constituency)
})
.catch((error) => {
console.log("error", error)
commit('SET_CONSTITUENCY', null)
})
}
actionTwo({commit}, constituency) {
console.log("actionTwo", constituency)
// do something
commit('COMMIT', 'Final value')
}
// Component
// You handle it with a computed property either referencing a getter or the store state.
{
computed: {
getConstituency(){
return this.$store.state.constituency
},
getSomeOtherConstituency(){
return this.$store.state.constituency.something / 3
}
},
// Optionally if you want to listen and react to changes use a `watcher`.
watch: {
// Gets excecuted each time getConstituency updates.
// ! Must have the same name.
getConstituency(update) {
// Do something, `update` is the new value.
}
}
}
Second Path: Handle data inside the component, then update the store.
Vue component.
methods: {
search_area_code(submitEvent) {
const parliament_id = this.parliament_id
const area_code = submitEvent.target.elements.area_code.value
axios('/cc-api/area-code/' + parliament_id + '/' + area_code)
.then(({data: constituency}) => {
this.$store.commit('SET_CONSTITUENCY', constituency)
// Do whatever you want with constituency now inside the component.
})
.catch((error) => {
console.log("error", error)
this.$store.commit('SET_CONSTITUENCY', null)
})
}
},
Notes:
$store.dispatch method returns a promise but still the constituency variable receives not the data fetched with the loadConstituencyByAreaCodeAndParliament action but remains empty.
When I enter the area code a second time everything works well.
I think the problem here is that you either handled bad the asyncronous code or trying to implement a custom pattern to work around.
As I said earlier put store getters in computed properties,
Look at this example in the Vuex-docs.
Code insights:
// Your action doesn't return anything, you must `return axios.get` inside it.
this.$store.dispatch('loadConstituencyByAreaCodeAndParliament', payload).then(() => {
let constituency = this.$store.getters.getConstituency()
})
// without the `return` statement the code above can be translated to
this.$store.dispatch('loadConstituencyByAreaCodeAndParliament', payload)
let constituency = this.$store.getters.getConstituency()
// If you opt for async a valid way would be
async doSomething(){
await this.$store.dispatch('loadConstituencyByAreaCodeAndParliament', payload)
let constituency = this.$store.getters.getConstituency()
}
// IF it still doesnt update anything try `$nextTick` https://vuejs.org/v2/api/
this.$nextTick(() => {
this.data = this.$store.getters.getConstituency()
})
I hope some of this has been helpful.

My Vue.js Vuex store has 2 actions that make GET requests. The second action requires the response from the first one in order to work. How to do it?

I have 2 actions that make GET requests and save the response in the Vuex store. The first action getVersion() gets the most recent version of the game and that version is required in order to make the second GET request. Right now I've hard coded the version in the second action, however, my goal is to concatenate it inside the URL.
Sadly I'm not sure how to access it from inside the function. Console.log(state.version) returns null for some reason even though it shouldn't be. I call these functions from inside App.vue like this:
mounted(){
this.$store.dispatch('getVersion')
this.$store.dispatch('getChampions')
}
Vuex store
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
version: null,
champions: null
},
mutations: {
version(state, data){
state.version = data.version
},
champions(state, data){
state.champions = data.champions
}
},
actions: {
getVersion({commit}){
axios.get("http://ddragon.leagueoflegends.com/api/versions.json")
.then((response) => {
commit('version', {
version: response.data[0]
})
})
.catch(function (error) {
console.log(error);
})
},
getChampions({commit, state}){
axios.get("https://ddragon.leagueoflegends.com/cdn/9.24.1/data/en_US/champion.json")
.then((response) => {
commit('champions', {
champions: response.data.data
})
})
.catch(function (error) {
console.log(error);
})
}
},
getters: {
version: (state) => {
return state.version;
},
findChampion: (state) => (id) => {
let championId = id.toString();
let champion = Object.values(state.champions).find(value => value.key === championId);
return champion
}
}
})
With this part:
this.$store.dispatch('getVersion')
this.$store.dispatch('getChampions')
The second dispatch doesn't wait for the first one to finish. Meaning that it is firing before the first one has had a chance to finish getting the version.
You need to create a promise that should resolve before the second dispatch is called.
You could try doing it this way:
async mounted(){
await this.$store.dispatch('getVersion')
await this.$store.dispatch('getChampions')
}
or if you don't want to use async/await
this.$store.dispatch('getVersion').then(() => {
this.$store.dispatch('getChampions');
});
And in the action you should add return to the request (this is important):
return axios.get(...
dispatcher returns a promise
this.$store.dispatch('getVersion').then(()=>{
this.$store.dispatch('getChampions');
});

How to make setState synchronous in React after axios response

I make HTTP request with axios and inside it I make another HTTP request in order to add some details about items that I get. I need to setState after I push it to the 'orders' Array, but it does it before so I can't print it in the correct way.
It works with SetTimeout but I want to do it in more professional way.
How can I do it synchronous??
fetchOrders(){
let orders = [];
let status;
this.setState({loading:true});
http.get('/api/amazon/orders')
.then(response => {
if (response.status === 200) status = 200;
orders = response.data;
orders.map(order => {
order.SellerSKU = "";
http.get(`/api/amazon/orders/items/${order.AmazonOrderId}`)
.then(res => {
order.SellerSKU = res.data[0].SellerSKU;
})
.catch(error => {
console.log(error);
})
});
setTimeout( () => {
this.setState({orders, error: status ? false : true, loading:false})
}, 1000);
})
.catch(error => {
this.setState({loading:false, error:true});
console.error(error);
});
}
You seem to be asking the wrong question, which is how to implement something async as sync, instead you really are trying to defer setting your state until everything is finished. (aka the XY Problem)
You don't need to make setState synchronous - you just need to leverage some promise chaining and Promise.all, which will allow you to defer the setState call until everything is finished.
In short you should be able to adapt this to something you need, applying a few more transformations to the responses:
fetchOrders() {
http.get('/first')
.then(r => r.data)
.then(orders => {
// This wraps the iterable of promises into another promise
// that doesn't resolve until all the promises in the iterable
// have finished.
return Promise.all(orders.map((order) => http.get(`/second/${order.id}`).then(/* transform */))
})
.then(transformedOrderPromises => this.setState({ ... });
}
setState can take a callback function, after finishing mutate the state it will execute the callback. So you can setState and add your 2nd API call in the callback.
Something like this:
http.get('/api/amazon/orders')
.then(response => {
if (response.status === 200) status = 200;
orders = response.data;
this.setState({orders, error: status ? false : true, loading:false},
() => {
orders.map(order => {
order.SellerSKU = "";
http.get(`/api/amazon/orders/items/${order.AmazonOrderId}`)
.then(res => {
order.SellerSKU = res.data[0].SellerSKU;
}).catch(error => {
console.log(error);
})
});
})
Please note that I just edited a dirty way, may be you need to make some adjustment to make it works.
In my projects, I fake a synchronous setState to avoid lot of pitfalls, making the code cleaner. Here's what I do:
class MyComponent extends React.Component {
// override react's setState
setState(partialState) {
const mergedState = { ...this.state, ...partialState };
this.state = mergedState;
super.setState(partialState);
}
}
Explained: before calling the true setState, the state is also set into this.state so that any reference to it before the actual update is correct.
The only downside is that you have to extend from MyComponent rather than React.Component.

Categories