Fetch runs double with old and updated value - reactjs

I have a fetch function as a component:
export const FetchBooksBySubject = (selectedValue) => {
const options = {
method: `GET`,
};
return fetch(`${server}/books?subjects_like=${selectedValue}`, options)
.then((response) => {
if(response.ok){
return response.json()
}
throw new Error('Api is not available')
})
.catch(error => {
console.error('Error fetching data: ', error)
})
}
This function I use in my App.js, where the idea is, to run fetch every time, the value gets updated:
useEffect(() => {
if(selectedValue) {FetchBooksBySubject(selectedValue).then(data => setBookList(data))};
}, [selectedValue])
const handleChange = e => {
setSelectedValue(e);
FetchBooksBySubject(selectedValue);
};
So it works, but it runs a doble request basically when I set the new value. Firts is for the value before the update and second for the updated value. Why? And is there any chance to run it only for the updated value?

First, FetchBooksBySubject is not a valid function component. Component should return React element.
const element = <h1>Hello, world</h1>;
FetchBooksBySubject is just a function returns a Promise, so you should rename it like fetchBooksBySubject.
Second, it is natural that your FetchBooksBySubject runs twice.
The first time is when the selectedValue changes. Check out official document about useEffect. When you provide some value in your dependency array, the values's change will trigger useEffect to run again.
useEffect(() => {
if(selectedValue) {FetchBooksBySubject(selectedValue).then(data => setBookList(data))};
}, [selectedValue])
The second time when fetching is called is after setSelectedValue, you are calling fetch function manually. So delete it if you don't need it.
const handleChange = e => {
setSelectedValue(e);
FetchBooksBySubject(selectedValue); // function called manually
};

Related

Refactoring useEffect to only require one database call

At the moment, I have a component which completes some backend calls to decide when to start displaying the UI.
It's structured like this:
useEffect(() => {
getData()
})
const getData = async () => {
await verifyUser()
await fetchData()
}
The purpose here, is that verifyUser() is supposed to run first, and in the response to verifyUser(), a user id is provided by the backend.
const verifyUser = async () => {
if (!localStorage.getItem('auth')) {
return
}
if (localStorage.getItem('auth')) {
await axios.post("/api/checkAuth", {
token: JSON.parse(localStorage.getItem('auth'))
})
.then((response) => {
return setUserId(response.data.user_id)
})
.catch((err) => {
console.log(err)
localStorage.removeItem('auth')
})
}
}
As a result of this, the fetchData() function is supposed to wait until the verifyUser() function has stopped resolving, so it can use the user id in the database query.
However, at the moment it...
Calls once, without the user id
Then calls again, with the user id (and therefore resolves successfully)
Here's the function for reference:
const fetchData = async () => {
console.log("Fetch data called.")
console.log(userId)
await axios.post("/api/fetch/fetchDetails", {
user_id: userId
})
.then((response) => {
// Sets user details in here...
return response
})
.then(() => {
return setFetching(false)
})
.catch((err) => {
console.log(err)
})
}
What I'm trying to achieve here is to essentially remove any concurrency and just run the functions sequentially. I'm not 100% sure what the best practice here would be, so some feedback would be appreciated!
Your useEffect is missing a dependency array argument:
useEffect(() => {
getData()
})
should be:
useEffect(() => {
getData()
}, [])
Without that argument, useEffect will run once each time your component renders. With that argument, it will only run once, when the component is first mounted (ie. after the first render).
If you needed it to depend on another variable (eg. user.id isn't defined on load, but is later on) you could put that variable in the dependency array, ie.
useEffect(() => {
if (!user.id) return;
getData()
}, [user.id])
This version would run once when the component is mounted, then again if the user.id changes (eg. if it goes from null to an actual number).
In React, the useEffect hook accepts two arguments - the first one is a function (this is the "effect"), and the second one is a dependency array. The simplest useEffect hook looks like this:
useEffect(() => {
}, [])
The above hook has no dependency (because the array is empty), and runs only when the component initially mounts, and then goes silent.
If you don't pass in a dependency array as the second argument, as #machineghost said, the hook will run the "effect" function every time your component re-renders.
Now to your specific problem. You want to run fetchData after verifyUser has resolved its Promise, so you'd add the outcome of verifyUser as a dependency to a separate useEffect hook that calls fetchData. In this case, the outcome is setting userId.
So instead of this:
useEffect(() => {
getData()
})
const getData = async () => {
await verifyUser()
await fetchData()
}
Do this:
useEffect(() => {
verifyUser();
}, []);
useEffect(() => {
if (userId) { // assuming userId has a false-y value before verifyUser resolved
await fetchData();
}
}, [userId])

Rendering different elements based on promise's result

I'm trying to make a login/logout button based on my user authentication status. since I'm fetching this data from my api, I cant seem to return the data itself, only the promise and since in reactjs the .then() function cant be used I don't know how to access the data I need.
this is the function that fetches the data from api, I want it to return "data.success" which is a boolean, but instead a promise is returned.
let checkAuth = () => {
return fetch('/v1/checkLogin')
.then(response => response.json())
.then(data => {
return data.success
})
}
react code calling above function :
{
(checkAuth())
? <button>logout</button>
: <button>login</button>
}
any help is appreciated =)
Due to the asynchronous nature of requests, the outer code will have already returned before the promise is resolved. That is why it returns a promise instead of your data.
A better approach to get through this is to use "useState" "useEffect" hooks
use "useEffect" to fetch the data when the component renders for the first time
use "useState" store the fetched data to a variable and use it in ways you want
export default LoginComponent = () => {
const [authenticated, setAuthenticated] = useState(false); // False initially
let checkAuth = () => {
const result = await fetch("/v1/checkLogin") // Wait for promise to resolve
const data = result.json();
setAuthenticated(data.success); // Set Data. (You can set just "data" if you want the whole data object)
};
// useEffect which fires once when the component initially renders
useEffect(() => {
checkAuth()
}, []);
return (
<div>
{authenticated ? <button>logout</button> : <button>login</button
</div>
);
};
You can use a state with useEffect to update the UI accordingly
const YourComponent = () => {
const [isAccess, setIsAccess] = useState(); // initialize your state
let checkAuth = () => {
return fetch("/v1/checkLogin")
.then((response) => response.json())
.then((data) => {
setIsAccess(data.success); //set the value for your state
});
};
useEffect(() => {
checkAuth()
}, [])
//after the state update, your UI will be re-rendered with the latest value which you expected
return (
<div>{isAccess ? <button>logout</button> : <button>login</button>}</div>
);
};

"Cannot read property 'issues' of undefined" Reactjs

I am using gitbeaker to get a project from gitlab API, after fetching the project, I used useState to save the project object, now I want to fetch another API whose URL is in that object, but whenever I try to access that URL, an error appears "Cannot read property 'issues' of undefined".
Here's my code:
const [project, setProject] = useState<any>({});
const api = new Gitlab({
host: "https://example.com",
token: "my token",
});
useEffect(() => {
(async () => {
const projectsPromises = await api.Projects.all().then((allprojects) => {
return allprojects;
});
Promise.all(projectsPromises).then((data) => {
setProject(data.find((element) => element.id === 338));
});
})();
return () => {};
}, []);
console.log(project);
console.log(project._links.issues);
fetch(project._links.issues).then((res) => console.log(res));
console.log(project); gives me {} and after some time it prints the object, that's why when I try to use project._links.issues it is undefined as I think it isn't resolved yet but I don't know how to make it work.
I solved it by fetching the data in the useEffect hook and saving the response of the api in the state so that I can access it later in my code, like that
const [issues, setIssues] = useState<any>([]);
Promise.all(projectsPromises).then((data) => {
const celoProject: any = data.find((element) => element.id === 338);
setProject(celoProject);
const projectIssues = fetch(celoProject._links.issues)
.then((res) => res.json())
.then((allIssues) => {
setIssues(allIssues);
});
});
If someone has a better way or an explanation why I couldn't access it outside the useEffect, please tell me.
Anything inside the useEffect hook will only execute when the page first loads (because you provided an empty array as the second argument). Anything outside of it will execute on every render (every time props or state changes). That is why it logs {} the first time because the effect is asynchronous and hasn't completed before the component is rendered.
You should run the second fetch in the useEffect hook after the first API request completes. Need more information to determine what exactly is happening beyond this.
const [project, setProject] = useState<any>({});
const api = new Gitlab({
host: "https://example.com",
token: "my token",
});
useEffect(() => {
(async () => {
const projectsPromises = await api.Projects.all().then((allprojects) => {
return allprojects;
});
Promise.all(projectsPromises).then((data) => {
const projectResponse = data.find((element) => element.id === 338)
setProject(projectResponse)
fetch(projectResponse._links.issues).then((res) => {
console.log(res)
// Do something with this response
});
});
})();
return () => {};
}, []);
console.log(project);
console.log(project._links.issues);

How to get value of `var` inside return of React component?

I'm trying to get the value of googleToken inside a <div> in the return of my React Component. The value is already updated, but it's the initial state here in the return, therefore, it always shows null
const Layout = props => {
let googleToken = null;
useEffect( () => {
fetchGoogleToken();
}, [])
const fetchGoogleToken = async () => {
await api
.get("/google_token")
.then((response) => {
console.log('google_token: ' + response.data.google_token);
googleToken = response.data.google_token;
console.log('google_token updated: ' + googleToken);
})
.catch((error) => console.log(error));
};
const getGoogleToken = (res) => {
console.log(res);
setGoogleToken(res.accessToken);
saveGoogleTokenInDB();
};
const saveGoogleTokenInDB = async () => {
await api
.post("/fit-auth", {google_token : googleToken})
.then((response) => {
console.log(response);
})
.catch((error) => console.log(error));
};
return (
<div className={classes.googleButton} style={{display: googleToken === null ? 'block' : 'none'}}>
<h3>{googleToken}</h3>
<div/>
}
Any ideas on why I can't get the updated value?
It is right to use useEffect hook for fetching. But the result must be kept into state. And when you ask react to update state, you can not watch changes on the next line of code using console.log because setState is async function and it will be executed later on.
// this will never work as you might be expected:
setState(newState)
console.log(state)
To catch state updates always use useEffect hook as in example below:
useEffect(() => {
console.log(state)
}, [state])
Also, avoid using inline styles for showing / hiding your components. Check the official conditional rendering recommendations.
The final code is going to look like this:
const Layout = props => {
const [googleToken, setGoogleToken] = useState(null)
useEffect( () => {
fetchGoogleToken();
}, [])
// you can watch how state changes only using useEffect:
useEffect(() => {
console.log('google_token updated: ' + googleToken)
}, [googleToken])
const fetchGoogleToken = async () => {
await api
.get("/google_token")
.then((response) => {
console.log('google_token: ' + response.data.google_token);
setGoogleToken(response.data.google_token);
})
.catch((error) => console.log(error));
};
// conditional rendering:
if (!googleToken) return <span>Fetching token...</span>
return (
<div className={classes.googleButton}>
<h3>{googleToken}</h3>
<div/>
)
}
Hope you will find this helpful.
The issue is that when the value of googleToken is updated the component is not notified about this. Since the component is not notified about the change it still believes that the value is still the initial value and it does need to change anything in DOM.
To solve this issue try using states or just force a rerender once the function is executed.

Axios and looped promises

I have problem with loop on axis GET request, and I can't understood why.
const [ state, setState ] = useState<any[]>([]);
ids.forEach((id) => {
getData(id)
.then((smth: Map<string, any>[]) => getNeededData(smth, id));
});
console.log(JSON.stringify(state));
and getData (getNeededData is only choose parameters):
export const getData= async (id: string) => {
const response = await Axios.get(`/rest/${id}`)
.then((res: { data: any; }) => res.data);
return response;
};
I should have 2 response (it's 2 id in variable "ids"), but I have first, second, first, second, first, and this in a loop.
Why it's been working like this?
What I can change for fix this?
By putting that forEach at the top level of your component function, you're running it every time the function is called by React to render its contents, which React does when state changes. The code you've shown doesn't set state, but I'm assuming your real code does.
To do it only when the component first mounts, wrap it in a useEffect callback with an empty dependency array:
const [ state, setState ] = useState<any[]>([]);
useEffect(() => {
ids.forEach((id) => {
getData(id)
.then(/*...*/);
});
}, []);
If all of the results are going in the state array, you probably want to use map and Promise.all to gether them all up and do a single state change with them, for instance:
const [ state, setState ] = useState<any[]>([]);
useEffect(() => {
Promise.all(
ids.map((id) => {
return getData(id).then(/*...*/);
})
)
.then(allResults => {
// Use `allResults` to set state; it will be an array in the same order
// that the `id` array was in
})
.catch(error => {
// handle/report error
});
}, []);

Resources