Refactoring breaks initial state - javascript

React (from create-react-app) with MobX. Using axios for async backend API calls.
This code works. The initial state (array of issues) is populated, and the webpage presenting this component renders with initial content from state.
import { observable, computed, autorun, reaction } from 'mobx'
import axios from 'axios'
class IssuesStore {
#observable issues = []
constructor() {
autorun(() => console.log("Autorun:" + this.buildIssues))
reaction(
() => this.issues,
issues => console.log("Reaction: " + issues.join(", "))
)
}
getIssues(data) {
return data.map((issue) => ({title: issue.name, url: issue.url, labels: issue.labels}))
}
#computed get buildIssues() {
const authToken = 'token ' + process.env.REACT_APP_GH_OAUTH_TOKEN
axios.get(`https://api.github.com/repos/${process.env.REACT_APP_GH_USER}/gh-issues-app/issues`,
{ 'headers': {'Authorization': authToken} })
.then(response => {
console.log(response)
this.issues = this.getIssues(response.data)
return this.issues
})
.catch(function(response) {
console.log(response)
})
}
}
export default IssuesStore
In an attempt to separate API invocation promises from individual components and stores, I pulled out the axios call into a separate js file, as a collection of functions:
import axios from 'axios'
const authToken = 'token ' + process.env.REACT_APP_GH_OAUTH_TOKEN
export function loadIssues() {
return this.apiPromise(
`https://api.github.com/repos/${process.env.REACT_APP_GH_USER}/gh-issues-app/issues`,
{ 'headers': {'Authorization': authToken} }
)
}
export function apiPromise(endpoint, options) {
return axios.get(endpoint, options)
.then((response) => {
// console.log("response: " + JSON.stringify(response, null, 2))
return response.data.map((issue) => ({title: issue.name, url: issue.url, labels: issue.labels}))
})
.catch(function(response) {
console.log(response)
})
}
Now, my store looks like this:
import { observable, computed, autorun, reaction } from 'mobx'
import * as github from '../api/github'
class IssuesStore {
#observable issues = []
constructor() {
autorun(() => console.log("Autorun:" + this.buildIssues))
reaction(
() => this.issues,
issues => console.log("Reaction: " + issues.join(", "))
)
}
#computed get buildIssues() {
this.issues = github.loadIssues().data
return this.issues
}
}
export default IssuesStore
Much smaller... but the webpage now throws an error because it now sees the initial state of issues as undefined on first render.
Uncaught TypeError: Cannot read property 'map' of undefined
The promise completes successfully later on (as it should), but by then it's too late. Sure, I can set up a few null checks in my rendering components to not run .map or other such functions on empty or as-yet-undefined variables.
But why does the code work with no initial rendering errors before the refactoring, and not after? I thought the refactoring was effectively maintaining the same logic flow, but I must be missing something?

In your refactored version
github.loadIssues().data
Is always going to be undefined because the data property on that Promise will always be undefined.
In the original version, this.issues was only ever set once data returned from the api, so the only values that it was ever set to were the initial value [] and the filled array from the api response.
In yours, the three states are [] -> undefined -> and the filled array.
buildIssues should look something like this:
#computed get buildIssues() {
github.loadIssues().then((data) => {
this.issues = data
}).catch((err) => {
// handle err.
})
}

Related

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');
});

Calling Axios API hit in a service file from Reducer Giving Error

In react I am trying to make a "Rest API call via Axios". I made a service file and then when reducer is trying to console.log the output of the service. it is giving error. Please help.
someReducer.js
import getItemsAPI from '../../services/service1';
...
case "GET_ITEM_LIST": {
let data = getItemsAPI.getItems();
console.log(data);
return {
...state,
items: data
}
}
service1.js
class getItemsAPI {
getItems() {
return this.axiosInstance
.get('https://jsonplaceholder.typicode.com/users/')
.then((response) => response.data);
}
}
export default getItemsAPI;
Error:
If you use a class, you must use the new keyword in order to create an instance. Then, you can use its methods:
import getItemsAPI from '../../services/service1';
const getItemsInstance = new getItemsApi();
...
case "GET_ITEM_LIST": {
let data = getItemsInstance.getItems();
console.log(data);
return {
...state,
items: data
}
}
You don't need to use a class in order to export a function. You can export the function itself (in this case, inside an object):
const getItemsAPI = {
getItems: () => {
return axiosInstance
.get('https://jsonplaceholder.typicode.com/users/')
.then((response) => response.data);
}
}
export default getItemsAPI;
If you use the code above, you don't need to create an instance. You can simply use the object (like your doing in the OP).
Just a note as well. getItems will return a Promise. In order to get data, you must await or resolve the Promise before reducing.

fetching json in seperate component

I've made an application and want to add more components which will use the same json I fetched in "personlist.js", so I don't want to use fetch() in each one, I want to make a separate component that only does fetch, and call it in the other components followed by the mapping function in each of the components, how can make the fetch only component ?
here is my fetch method:
componentDidMount() {
fetch("data.json")
.then(res => res.json())
.then(
result => {
this.setState({
isLoaded: true,
items: result.results
});
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
error => {
this.setState({
isLoaded: true,
error
});
}
);
}
and here is a sandbox snippet
https://codesandbox.io/s/1437lxk433?fontsize=14&moduleview=1
I'm not seeing why this would need to be a component, vs. just a function that the other components use.
But if you want it to be a component that other components use, have them pass it the mapping function to use as a prop, and then use that in componentDidMount when you get the items back, and render the mapped items in render.
In a comment you've clarified:
I am trying to fetch the json once, & I'm not sure whats the best way to do it.
In that case, I wouldn't use a component. I'd put the call in a module and have the module expose the promise:
export default const dataPromise = fetch("data.json")
.then(res => {
if (!res.ok) {
throw new Error("HTTP status " + res.status);
}
return res.json();
});
Code using the promise would do so like this:
import dataPromise from "./the-module.js";
// ...
componentDidMount() {
dataPromise.then(
data => {
// ...use the data...
},
error => {
// ...set error state...
}
);
}
The data is fetched once, on module load, and then each component can use it. It's important that the modules treat the data as read-only. (You might want to have the module export a function that makes a defensive copy.)
Not sure if this is the answer you're looking for.
fetchDataFunc.js
export default () => fetch("data.json").then(res => res.json())
Component.js
import fetchDataFunc from './fetchDataFunc.'
class Component {
state = {
// Whatever that state is
}
componentDidMount() {
fetchFunc()
.then(res => setState({
// whatever state you want to set
})
.catch(err => // handle error)
}
}
Component2.js
import fetchDataFunc from './fetchDataFunc.'
class Component2 {
state = {
// Whatever that state is
}
componentDidMount() {
fetchFunc()
.then(res => setState({
// whatever state you want to set
})
.catch(err => // handle error)
}
}
You could also have a HOC that does fetches the data once and share it across different components.

Vuejs with axios request in vuex store: can't make more than one request, why?

I'm building some vuejs dashboard with vuex and axios, between others, and I've been struggling for a while on a pretty pesky problem: it seems I can't make more than one request! All subsequent calls fail with this error:
Fetching error... SyntaxError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': 'Bearer {the_entire_content_of_the_previous_api_response}' is not a valid HTTP header field value.
My store looks like that:
import axios from "axios";
const state = {
rawData1: null,
rawData2: null
};
const actions = {
FETCH_DATA1: ({ commit }) =>
{
if (!state.rawData1)
return axios.get("/api/data1")
.then((response) =>
{
commit("SET_RAW_DATA1", response.data);
});
},
FETCH_DATA2: ({ commit }) =>
{
if (!state.rawData2)
return axios.get("/api/data2")
.then((response) =>
{
commit("SET_RAW_DATA2", response.data);
});
}
};
const mutations = {
SET_RAW_DATA1: (state, data) =>
{
state.rawData1 = data;
},
SET_RAW_DATA2: (state, data) =>
{
state.rawData2 = data;
}
};
export default
{
namespaced: true,
state,
actions,
mutations
};
I don't think my API has any problem, as everything seems to work smoothly via Postman.
Maybe it's obvious for some, but I can't spot what's the matter as I'm still quite a vue noob...
Oh, and I'm handling the axios Promise like this, if this is of any interest:
this.$store.dispatch("api/FETCH_DATA1").then(() =>
{
// parsing this.$store.state.api.rawData1 with babyparse
}).catch((err) =>
{
this.errorMsg = "Fetching error... " + err;
});
After #wajisan answer, it does seem to work with "traditional" calls, but not with fetching file calls. I've tried stuff with my Echo api, to no avail... More details there: Serving files with Echo (Golang).
Any ideas, pretty please? :)
your code seems very correct, i think that your problem is from the API.
You should try with another one, just to make sure :)
Well, played a bit more with axios config and manage to make it work (finally!).
I just created a axios instance used by my store, and the weird header problem thingy disappeared! I'm not exactly sure why, but seems to be because of some things going on in the default axios config between my calls...
Even if not much has changed, the new store code:
import axios from "axios";
const state = {
rawData1: null,
rawData2: null
};
const api = axios.create({ // Yep, that's the only thing I needed...
baseURL: "/api"
});
const actions = {
FETCH_DATA1: ({ commit }) =>
{
if (!state.rawData1)
return api.get("/data1") // Little change to use the axios instance.
.then((response) =>
{
commit("SET_RAW_DATA1", response.data);
});
},
FETCH_DATA2: ({ commit }) =>
{
if (!state.rawData2)
return api.get("/data2") // And there too. Done. Finished. Peace.
.then((response) =>
{
commit("SET_RAW_DATA2", response.data);
});
}
};
const mutations = {
SET_RAW_DATA1: (state, data) =>
{
state.rawData1 = data;
},
SET_RAW_DATA2: (state, data) =>
{
state.rawData2 = data;
}
};
export default
{
namespaced: true,
state,
actions,
mutations
};
Hope that'll help someone!

Categories