React does not wait for async function before mounting components - reactjs

I am currently using a mock json-server to hold user information in my React app. I am working on storing settings and preferences for users. I have a setting page implemented through a Route component. I am displaying the settings configurations on this page. I am fetching the user settings in App.tsx :
const fetchUser = async (id:number) => {
const res = await fetch(`http://localhost:5001/users/${id}`)
const user = await res.json()
return user
}
const getSettings = async () => {
const user = await fetchUser(0)
setSettings(user.settings);
}
Then I am passing down the state variable for settings through useContext.
const [settings, setSettings] = useContext(userContext);
This works fine when I start on the root page and then go to the settings. However, if the user goes directly to the settings page, the setting state is initially null and I cannot access its values. I tried to fetch the settings again in the setting page component with useEffect but React does not wait for async functions to complete before mounting the components.
const getSettings = async (id:number) => {
const res = await fetch(`http://localhost:5001/users/${id}`)
const user = await res.json()
const settings = user.settings
setSettings(settings);
}
useEffect(() => {
if (!settings) getSettings(0);
}, [])
Is there a way to get around this? I would like to access the settings state throughout the app but the user should not have to start with the root component.
Note: It does work if I check that the value is null before use like this :
settings?.test.difficulty

Please try the below change for useEffect:
useEffect(async () => {
if (!settings) await getSettings(0);
}, [])

Related

React Query: rerun onSuccess method of a cached query

What I have
I am getting the user's location (latitude/longitude) which I use to call a google geocode API, unless the user's coords change, the request is not running again, since the query it uses the user's coords as queryKey array dependecy.
The problem
the problem is that I'm running some operations in the onSuccess query method, this method is only run when any of the queryKey dependencies change, and I mentioned this not happen.
How to run the onSuccess method whether the queryKey dependencies change or not?
Reference code
export const useGoogleReverseGeocoding = (coords) => {
const url = 'someUrl';
const request = createClient(); // axios abstraction
return useQuery({
queryKey: ['google-geocode', coords],
queryFn: request,
enabled: !!coords,
onSuccess: (data) => {
const searchTerm = removeGlobalCodeText(data?.plus_code?.compound_code);
// set searchterm in a global store. This searchterm change with
// different user actions, so if the user re-share his location
// I need to run onSuccess transformation again.
setSearchTerm(searchTerm);
},
});
};
As I was explaining in my comment, onSuccess can't be fired without the query itself firing again. Since certain user actions should trigger the transformations on onSuccess, you have a couple of ways to go about this, one of them would be to move these transformations on a useEffect hook and add some user action related flag on the dependencies array. The other proposed solution would be to invalidate the query upon these user actions, so it will be refetched and the transformations on onSuccess will execute.
You can achieve this using useQueryClient hook which returns the current QueryClient instance. You can invalidate the query from anywhere as long as the component is wrapped by QueryClientProvider. For this example and for convenience, I will include this hook on useGoogleReverseGeocoding custom hook.
Example:
Custom hook:
export const useGoogleReverseGeocoding = (coords) => {
const queryClient = useQueryClient()
const url = 'someUrl';
const request = createClient(); // axios abstraction
const geocodingData = useQuery({
queryKey: ['google-geocode', coords],
queryFn: request,
enabled: !!coords,
onSuccess: (data) => {
const searchTerm = removeGlobalCodeText(data?.plus_code?.compound_code);
// set searchterm in a global store. This searchterm change with
// different user actions, so if the user re-share his location
// I need to run onSuccess transformation again.
setSearchTerm(searchTerm);
},
});
const invalidateQueryOnAction = () => queryClient.invalidateQueries(['google-geocode'])
return { geocodingData, invalidateQueryOnAction }
};
Some component:
const dummyCoords = {
lat: 33.748997,
lng: -84.387985
}
const SomeComponent = () => {
const { geocodingData, invalidateQueryOnAction } =
useGoogleReverseGeocoding(dummyCoords)
const handleSomeUserAction = () => {
// handle action...
// Invalidate query, so the query gets refetched and onSuccess callback executes again
invalidateQueryOnAction()
}
}
PS: If #TkDodo comes along with a different solution for this, I would suggest to go for it instead.

when I try to upload an image on firestore I get the url but it is not stored in the firestore database

const CreatePost = async () => {
if (imageUpload == null) return;
const imageRef = ref(storage, `images/`);
uploadBytes(imageRef, imageUpload).then((snapshot) => {
getDownloadURL(imageRef).then((url) => {
setImageUrl(url);
console.log(url);
});
});
console.log(url) Works and gives me the correct url but it is not stored in firestore
React useState hook is asynchronous. So, it won't have the URL that you are setting at the time when you're using it. Use the useEffect hook and add your state in the dependence array, and you will always get the updated value.

react navigate and save history

I am building a React Application with multi-router
Home router call an API in use Effect but when I navigate to another Route and go back to home the request is recall and the component which contain response is reload
is there a way to save history so when I come back the route not calling the API and if it call it, at least not reload the section using response
here my Use-effect
useEffect(() => {
(async () => {
try{
const response = await axios.get("user")
dispatch(setAuth(response.data))
}
catch(e){}
try{
const response = await axios.get("get_all_posts")
setpostsInfo(response.data)
}
catch(e){}
})()
}, []);
Thanks for help
add this isRun
const [ isRun ,setIsRun ] =useState(true)
useEffect( () => {
if(isRun){
(async () => {
try{
const response = await axios.get("user")
dispatch(setAuth(response.data))
}
catch(e){}
try{
const response = await axios.get("get_all_posts")
setpostsInfo(response.data)
setIsRun(false)
}
catch(e){}
})()
}
}, []);
when you change the route you component unmount so its state is lost.
when you go back to the home route the component mount again it's a new instance so you can't hold the information in the component you should hold the information of the number of visiting the page for example or if it's the first time mounting the component in a higher place than the component (the localstorage for example) you can store a key or value to indicate that it's the first time visiting this page and when the compoenent unmount the information stills there. when the component mount again check the existance and validity of the key in the localstorage and you decide whether you send the request or not in the useEffect

How to setup a function which gets app settings and sets it as localStorage before the page loads. (next.js)

I've been working on a Next.JS web application for the past couple of days but I've reached a problem. The app has an API call (/api/settings) which returns some settings about the application from the database. Currently, I have a function which returns these settings and access to the first component:
App.getInitialProps = async () => {
const settingsRequest = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/settings`
);
const settingsResponse = await settingsRequest.json();
return { settings: settingsResponse };
};
This does work and I am able to pass in settings to components but there are two problems with this:
I need to nest the prop through many components to reach the components that I need
This request runs every time a page is reloaded/changed
Essentially, I need to create a system that does this:
runs a function in the _app.tsx getInitialProps to check if the data is already in localStorage, if not make the API request and update localStorage
have the localStorage value accessible from a custom hook.
Right now the problem with this is that I do not have access to localStorage from the app.tsx getInitialProps. So if anyone has an alternative to run this function before any of the page loads, please let me know.
Thanks!
I found a solution, it might be a janky solution but I managed to get it working and it might be useful for people trying to achieve something similar:
First we need to create a "manager" for the settings:
export const checkIfSettingsArePresent = () => {
const settings = localStorage.getItem("app_settings");
if (settings) return true;
return false;
};
export const getDataAndUpdateLocalStorage = async () => {
const r = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/settings`);
const response = await r.json();
localStorage.setItem("app_settings", JSON.stringify(response));
};
With that created we can add a UseEffect hook combined with a useState hook that runs our function.
const [doneFirst, setDoneFirst] = useState<boolean>(false);
useEffect(() => {
const settingsPreset = checkIfSettingsArePresent();
if (performance.navigation.type != 1)
if (settingsPreset) return setDoneFirst(true);
const getData = async () => {
await getDataAndUpdateLocalStorage();
setDoneFirst(true);
};
getData();
}, []);
//any other logic
if (!doneFirst) {
return null;
}
The final if statement makes sure to not run anything else before the function.
Now, whenever you hot-reload the page, you will see that the localStorage app_settings is updated/created with the values from the API.
However, to access this more simply from other parts of the app, I created a hook:
import { SettingsType } from "#sharex-server/common";
export default function useSettings() {
const settings = localStorage.getItem("app_settings") || {
name: "ShareX Media Server",
};
//#ts-ignore
return JSON.parse(settings) as SettingsType;
}
Now I can import useSettings from any function and have access to my settings.

Component shows previous data when mount for fractions of seconds

I am developing an app named "GitHub Finder".
I am fetching the date in App component using async function and pass these function to User component as props and I call these functions in useEffect.
The problem is here, when I goto user page for second time it shows previous data which I passed in props from App component and then it shows loader and shows new data.
Here is App component code where I am fetching date from APIs and passing to User component through props.
// Get single GitHub user
const getUser = async (username) => {
setLoading(true);
const res = await axios.get(
`https://api.github.com/users/${username}?client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID}&client_secret=${
process.env.REACT_APP_GITHUB_CLIENT_SECRET}`
);
setUser(res.data);
setLoading(false);
}
// Get user repos
const getUserRepos = async (username) => {
setLoading(true);
const res = await axios.get(
`https://api.github.com/users/${username}/repos?
per_page=5&sort=created:asc&client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID}&client_secret=${
process.env.REACT_APP_GITHUB_CLIENT_SECRET}`
);
setRepos(res.data);
setLoading(false);
}`
User component code.
useEffect(() => {
getUser(match.params.login);
getUserRepos(match.params.login);
// eslint-disable-next-line
}, []);
I've recorded a video, so you guys can easily understand what I am trying to say.
Video link
Check live app
How can I solve this problem?
Thank in advance!
Here is what happens in the app :
When the App component is rendered the first time, the state is user={} and loading=false
When you click on a user, the User component is rendered with props user={} and loading=false, so no spinner is shown and no data.
After the User component is mounted, the useEffect hooks is triggered, getUser is called and set loading=true (spinner is shown) then we get the user data user=user1 and set loading=false (now the user data is rendered)
When you go back to search page, the app state is still user=user1 and loading=false
Now when you click on another user, the User component is rendered with props user=user1 and loading=false, so no spinner is shown and the data from previous user is rendered.
After the User component is mounted, the useEffect hooks is triggered, getUser is called and set loading=true (spinner is shown) then we get the user data user=user2 and set loading=false (now the new user data is rendered)
One possible way to fix this problem :
instead of using the loading boolean for the User component, inverse it and use loaded
When the User component is unmounted clear the user data and the loaded boolean.
App component:
const [userLoaded, setUserLoaded] = useState(false);
const getUser = async username => {
await setUserLoaded(false);
const res = await axios.get(
`https://api.github.com/users/${username}?client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID
}&client_secret=${process.env.REACT_APP_GITHUB_CLIENT_SECRET}`
);
await setUser(res.data);
setUserLoaded(true);
};
const clearUser = () => {
setUserLoaded(false);
setUser({});
};
<User
{...props}
getUser={getUser}
getUserRepos={getUserRepos}
repos={repos}
user={user}
loaded={userLoaded}
clearUser={clearUser}
/>
User component:
useEffect(() => {
getUser(match.params.login);
getUserRepos(match.params.login);
// eslint-disable-next-line
return () => clearUser();
}, []);
if (!loaded) return <Spinner />;
You can find the complete code here
Please make your setUser([]) empty at the start of getUser like this:
const getUser = async (username) => {
setLoading(true);
setUser([]);
const res = await axios.get(
`https://api.github.com/users/${username}?client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID}&client_secret=${
process.env.REACT_APP_GITHUB_CLIENT_SECRET}`
);
setUser(res.data);
setLoading(false);
}

Resources