React and Firebase: onAuthStateChanged problem, state update on unmounted component - reactjs

This is my Context file, and I have my App.js wrapped with the AuthProvider:
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, user => setUser(user));
return () => { unsubscribe(); }
}, []);
return (
<AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
);
};
This is one of those consumer component.
function PurchasesNSales() {
const { user } = useContext(AuthContext);
const history = useNavigate();
useEffect(() => {
if (!user)
history("/", { replace: true });
}, [user, history]);
return(
<p>Welcome {user?.displayName}</p>
);
}
export default PurchasesNSales;
If I remove the useEffect on the second file, everything works fine, but if I do that I am not verifying authentication of the user.
I am most likely doing something wrong.
From what I understand I am trying to setState on a component that is already unmounted, which I don't understand where that is happening.
I am on my Dashboard page, I press the button which should take me to the "PurchasesNSales" page and that is when the error occurs.
Codesandbox Hopefully this sample app helps.

Related

Wait for context value to load before making an API call

I have a user context that loads the user data. I am using that data to send API requests in useEffect. The time lag in the loading of the data is causing an undefined variable in my API request. How do I make the useEffect wait for the context variable to load before sending the request?
This is the UserContext.js:
import { createContext, useState } from "react";
const UserContext = createContext({});
export const UserProvider = ({ children }) => {
const [user, setUser] = useState({});
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
export default UserContext;
This is the custom hook:
import { useContext } from "react";
import UserContext from "../context/UserProvider";
const useUser = () => {
return useContext(UserContext);
};
export default useUser;
And this is the API call in the profile page:
const { user } = useUser();
useEffect(() => {
Axios.get(
`API_URL/${user?.subscription_id}`
).then((res) => {
console.log(res)
});
}, []);
How can I ensure user data is loaded before I make a request throughout my app?
In react, Context APi static data will be Passed to the Children at initial Load. But if you are using asynchronous data in context api, you have to use useEffect and add context value as dependency..
export const UserProvider = ({ children }) => {
const [user, setUser] = useState({});
// Updating Context value Asynchronously..
setTimeout(() => {
setUser("data");
}, [3000]);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
const { user } = useUser();
useEffect(() => {
// Call api only if user contains data.
if(user != {}) {
Axios.get(
`API_URL/${user?.subscription_id}`
).then((res) => {
console.log(res)
});
}
}, [user]);

(React JS Hooks issue) React JS only render posts from logged in user

Problem
I am new to React and am trying to build an application whereby logged in users can view posts they have created. I am having issues with asynchronous functions causing variables to be accessed before they are loaded in. I am using a Firestore database.
Code
I followed this tutorial to set up authentication. I have created an AuthContext.js file, which contains this code (reduced):
const AuthContext = createContext();
export const AuthContextProvider = ({children}) => {
const [user, setUser] = useState({});
// const googleSignIn = () => {...}
// const logOut = () => {...}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser);
});
return () => {
unsubscribe();
}
}, []);
return (
<AuthContext.Provider value={{ googleSignIn, logOut, user }}>
{children}
</AuthContext.Provider>
)
};
export const UserAuth = () => {
return useContext(AuthContext);
}
I then wrap my application with a AuthContextProvider component and import UserAuth into any component that I want to be able to access the user object from. I have a PostPage component, and in it I want to ONLY render posts created by the logged in user. Each post has a user property containing the uid of the author. Here is my code:
import { UserAuth } from './context/AuthContext'
const PostsPage = () => {
const { user } = UserAuth();
const [posts, setPosts] = useState([]);
const postsRef = collection(db, 'posts');
useEffect(() => {
const getData = async () => {
if (user) {
const q = query(postsRef, where('user', '==', user.uid));
const data = await getDocs(q);
const filtered = data.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
setPosts(filtered);
}
}
return () => {
getData();
}
}, [user]);
return (
// Display posts
)
}
export default PostsPage;
Upon immediately refreshing the page, getData is executed. However, the code wrapped in the if statement does not run because the user has not yet been loaded in. Yet despite the dependancy array, getData is not executed again once the user data loads in, and I can't figure out why. If I render the user's uid, e.g. <p>{ user.uid }</p>, it will soon appear on the screen after the data has been loaded. But, I cannot figure out how to trigger getData after the user has been loaded. Any help with this would be much appreciated, thanks.
You have an issue just because you put getData() call to the cleanup function of a hook. Cleanup function will execute on depsArray change but it will be executed with old data, closure captured. So when user changes from undefined => any - getUser will be called and will still have a closure-captured user set to undefined. You can clear the array instead in it, so if user logs out - dont show any messages
useEffect(() => {
const getData = async () => {
if (!user) return;
const q = query(postsRef, where("user", "==", user.uid));
const data = await getDocs(q);
const filtered = data.docs.map((doc) => ({
...doc.data(),
id: doc.id
}));
setPosts(filtered);
};
getData().catch(console.error);
return () => {
setPosts([]);
};
}, [user]);

Why does my image in Firebase Storage render only after reload 404 error

I am able to upload the image successfully to Firebase Storage, but when I try to render it back to my dashboard I get a 404 error, file not found. I figure it has to do with the database not uploading the image and then sending it back to my react app. When I reload the page, the image renders. I have an async/await function for setting the userImage. What is the best way to manage this? New to React btw.
import { useState, createContext, useEffect } from "react";
import app from "../firebase";
const UserContext = createContext();
const UserProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [userImg, setUserImage] = useState(null);
console.log("userImg from context", userImg);
const img = async () => {
const imgPath = await app.firebase
.storage()
.ref(`users/${user.uid}/profile.jpg`)
.getDownloadURL();
setUserImage(imgPath);
};
useEffect(() => {
if (user) img();
}, [user]);
useEffect(() => {
app.auth().onAuthStateChanged(setUser);
}, [user]);
return (
<UserContext.Provider value={[user, setUser, userImg, setUserImage]}>
{children}
</UserContext.Provider>
);
};
export { UserContext, UserProvider };
The problem with your code is that the component gets rendered before even the img is retrieved from the server.
So what you can do is have a loading state in your component and until the img isn't received back from the server set the loading state to true.
import { useState, createContext, useEffect } from "react";
import app from "../firebase";
const UserContext = createContext();
const UserProvider = ({ children }) => {
const [loading, setLoading] = useState(false) // <--------Loading state--------
const [user, setUser] = useState(null);
const [userImg, setUserImage] = useState(null);
console.log("userImg from context", userImg);
const img = async () => {
setLoading(true) // Setloading to true
const imgPath = await app.firebase
.storage()
.ref(`users/${user.uid}/profile.jpg`)
.getDownloadURL();
setUserImage(imgPath);
setLoading(false) // <--------setting loading to false after receiving the image-------
};
useEffect(() => {
if (user) img();
}, [user]);
useEffect(() => {
app.auth().onAuthStateChanged(setUser);
}, [user]);
if(loading) {
return (<div>Loading...</div>) // <-----return loading if loading is true----
}
return (
<UserContext.Provider value={[user, setUser, userImg, setUserImage]}>
{children}
</UserContext.Provider>
);
};
export { UserContext, UserProvider };

React Hook Function in UseEffect with infinite loop

I am trying to call a hook in my App.js file using a hook. All the logic works, but I'm getting a warning error in console "React Hook useEffect has a missing dependency: 'initAuth'." I know there are a lot of issues on this ,but I'm not sure if this is related to the hook or the complexity I am doing at the high level of my app. The intent is to use the "initAuth" function to look at my local storage and get my user token, name, etc... I only want this on a hard page refresh, so it should only run once.
If I add initAuth (the function) or the authObject ( object), I get infinite loops.
function App() {
const { initAuth, authObject } = useAuth();
useEffect(() => {
initAuth();
}, []);
// this throws the warning. I need to add dependency
}
If you only want this effect to run once when the component first loads, then you can ignore the warning. You can disable the warning so it doesn't keep showing up in the console with the following:
useEffect(() => {
initAuth();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
This is how I would implement this hook :
function App() {
const { initialized, authObject, initAuth } = useAuth();
useEffect(() => {
if (!initialized) {
initAuth();
}
}, [initialized, initAuth]);
...
}
Or, better yet :
function App() {
const authObject = useAuth(); // let useAuth initialize itself
...
}
Typically, useAuth seems to be a multi-purpose hook, being used by various components, so it makes no sense to allow multiple components to call initAuth; the hook should only return the current state.
Preferably, you should implement that hook with a context
function App() {
return (
<AuthProvider>
<AppContent />
</AuthProvider>
);
}
function AppContent() {
const authObject = useAuth();
...
}
The contract, therefore, goes to the AuthProvider, and notifies every component using useAuth on state changes.
From OP's own answer, added some suggested improvements :
import React, { createContext, useContext, useState, useMemo } from "react";
const AuthContext = createContext({
isLoggedIn:false /* :Boolean */,
authObject:null /* :Object */,
login: (
username /* :String */,
password /* :String */
) /* :Preomise<Boolean> */ => {
throw new Error('Provider missing');
}
]);
const AuthContextProvider = ({ children }) => {
// init state with function so we do not trigger a
// refresh from useEffect. Use useEffect if the
// initial state is asynchronous
const [state, setState] = useState(() => {
const authObject = localStorage.getItem("authObject");
const isLoggedIn = !!authObject;
return { isLoggedIn, authObject };
});
// avoid refresh if state does not change
const contextValue = useMemo(() => ({
...state, // isLoggedIn, authObject
login: async (username, password) => {
// implement auth protocol, here
// do not expose setState directly in order to
// control what state is actually returned
// setState({ isLoggedIn:..., authObject:... });
// return true|false
}
}), [state]);
return (
<AuthContext.Provider value={ contextValue }>
{ children }
</AuthContext.Provider>
);
};
/**
Usage: const { isLoggedIn, authObject, login } = useAuthContext();
*/
const useAuthContext = () => useContext(AuthContext);
export { useAuthContext, AuthContextProvider };
Thanks to Yanick's comment, this is how I initiated to provider to set my authorization. My login function uses an auth service for http call, but I use this context function to set the data properly.
import React, { useContext, useMemo, useState } from "react";
import http from "services/http";
const AuthContext = React.createContext({});
const AuthContextProvider = ({ children }) => {
const [state, setState] = useState(() => {
const authObject = JSON.parse(localStorage.getItem("authObject"));
if (authObject) {
//sets axios default auth header
http.setJwt(authObject.token);
}
const isLoggedIn = !!authObject;
return { isLoggedIn, authObject };
});
// avoid refresh if state does not change
const contextValue = useMemo(
() => ({
...state, // isLoggedIn, authObject
login(auth) {
localStorage.setItem("authObject", JSON.stringify(auth));
http.setJwt(auth.token);
setState({ authObject: auth, isLoggedIn: true });
return true;
},
logout() {
http.setJwt("");
localStorage.removeItem("authObject");
setState({ authObject: null, isLoggedIn: false });
},
}),
[state]
);
return (
<AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
);
};
const useAuthContext = () => useContext(AuthContext);
export { useAuthContext, AuthContextProvider };
And my App.js simply uses the ContextProvider, no need to run useEffect anymore on App.js.
<AuthContextProvider>
<ThemeProvider theme={darkState ? dark() : light()}>
<CssBaseline>
<BrowserRouter>
//...app.js stuff
</BrowserRouter>
</CssBaseline>
</ThemeProvider>
</AuthContextProvider>
In any component, I can now get access to isLoggedIn or authObject using a call like:
const { isLoggedIn } = useAuthContext();

How can I get data from AsyncStorage and add it to initial state when using useReducer?

I'm using useContext & useReducer from Hooks & AsyncStorage.setItem in useEffect to save the data whenever state gets updated. On app reload I want to make sure to get saved data using AsyncStorage.getItem and add it to the initial state.
I tried to add init function with async as the third property to useReducer but still, it's not replacing initial data with received data. Please go through below code and help.
Thank you in advance!
Current code where I can save data to AsyncStorage
const [user, dispatch] = useReducer(userReducer, {});
useEffect(() => {
AsyncStorage.setItem('user', JSON.stringify(user))
}, [user]);
return(
<UserContext.Provider value={{user,dispatch}}>
{props.children}
</UserContext.Provider>
);
}
Below is the code I tried, but unable save existing data as initial data.
const getUser = async function() {
const userData = await AsyncStorage.getItem('user')
console.log("parse");
console.log(userData);
console.log("parsed data");
console.log(JSON.parse(userData));
return userData ? JSON.parse(userData) : {};
}
export const UserContext = createContext();
const UserContextProvider = (props) => {
const [user, dispatch] = useReducer(userReducer, {}, getUser);
useEffect(() => {
console.log("context");
console.log(JSON.stringify(user));
AsyncStorage.setItem('user', JSON.stringify(user))
}, [user]);
return(
<UserContext.Provider value={{user,dispatch}}>
{props.children}
</UserContext.Provider>
);
}
Thank you!
Updated and working code based on below suggestion and with minor changes.
const getUser = async () => {
try {
const user = await AsyncStorage.getItem('user')
return user ? JSON.parse(user) : {};
} catch (e) {
console.log('Failed to fetch the data from storage');
}
}
export const UserContext = createContext();
const UserContextProvider = (props) => {
const [user, dispatch] = useReducer(userReducer, {});
// Loading initial Satte
useEffect(() => {
async function fetchUser() {
const user = await getUser();
dispatch({type: 'ADD_USER', user});
}
fetchUser();
}, []);
// Update AsyncStorage when user is updated
useEffect(() => {
// This check is required to avoid initial writing to asyncStorage
if(user) {
AsyncStorage.setItem('user', JSON.stringify(user))
}
}, [user]);
return(
<UserContext.Provider value={{user,dispatch}}>
{props.children}
</UserContext.Provider>
);
}
Your initial state to useReducer needs to synchronous. Since asyncStorage is not a synchronous API you can't actually pass the value as initialState
You however can make use of useEffect loading state like below
const getUser = async () => {
try {
const user = await AsyncStorage.getItem('user')
return user ? JSON.parse(user) : {};
} catch (e) {
console.log('Failed to fetch the data from storage');
}
}
export const UserContext = createContext();
const UserContextProvider = (props) => {
const [isLoading, setIsLoading] = useState(true);
const [user, dispatch] = useReducer(userReducer, {});
// Loading initial Satte
useEffect(() => {
async function fetchUser() {
const user = await getUser();
setIsLoading(false);
dispatch({type: 'INIT_REDUCER', user}); // You must handle initReducer in reducer
}
fetchUser();
}, []);
useEffect(() => {
if(user) {
// This check is required to avoid initial writing to asyncStorage
console.log("context");
console.log(JSON.stringify(user));
AsyncStorage.setItem('user', JSON.stringify(user))
}
}, [user]);
if(isLoading) {
return (
<View>
<Text>Loading...</Text>
</View>
);
}
return(
<UserContext.Provider value={{user,dispatch}}>
{props.children}
</UserContext.Provider>
);
}

Resources