So I have Dashboard component which should display currentUsers info.
I keep my currentUser in global context/state.
The problem is when Dashboard component renders FIRST time currentUser is null even tho user is actually logged in. Because of that I get null error at line 14 or if I comment dashboard states the if statement on line 22 will happen and it will redirect me even if user is logged in.
Is there a way to await for isLoggedInFunction to finish and then do the logic in Dashboard component?
In App.js
export const CurrentUserContext = React.createContext(null); // Global Context
const [currentUser, setCurrentUser] = useState(null); // Global State
// Setting current user
useEffect(() => {
setCurrentUser(isLoggedIn()).catch(err => console.log(err));
}, []);
// IsLoggedIn Method
export function isLoggedIn() {
return localStorage.getItem('user') === null ?
null :
JSON.parse(localStorage.getItem('user'));
}
<CurrentUserContext.Provider value={{ currentUser, setCurrentUser }}> // Provider
You can set the default value for the user to avoid the first error.
In this case you can change your Dashboard like this:
/* ... */
const {
currentUser = {
isLoggedIn: false,
firstName: 'first name'
}
} = useContext(CurrentUserContext);
const [firstName, setFirstName] = useState(currentUser.firstName);
/* ... */
if (!currentUser.isLoggedIn) {
return <Redirect to={'/login'}/>;
}
/* ... */
To solve the second problem, you need to check if your localStorage actually contains the user field after authorization.
I fixed both problems by doing this.
const [currentUser, setCurrentUser] = useState(null); // Replaced this line with next line
const [currentUser, setCurrentUser] = useState(() => isLoggedIn());
And I removed useeffect since I don't need it anymore.
// Setting current user
useEffect(() => {
setCurrentUser(isLoggedIn()).catch(err => console.log(err));
}, []);
Related
I'm currently building an app which allows a user to collect stamps by scanning a QR code. I have a Firebase Firestore snapshotlistener attached which listens for new stamps being added to that user. And rather than to fetch the new total amount of stamps for that user, I wanted to use the payload of the snapshot and add it to the list of stamps which is saved in a Context.
Unfortunately, this only works the first time when the app has been started. The procedure looks like this:
QR code gets scanned
New snapshot is received with newly added stamps (1 or more)
I'm logging the current state of total stamps to console, which looks OK.
The new stamps get added to the state context.
Logging the context state changes show the stamps were added correctly.
But the second time or any after that it looks like this:
QR code gets scanned
New snapshot is received with newly added stamps (1 or more)
I'm logging the current state of total stamps to console, but this still looks the same as before the first attempt.
The new stamps get added to the old state context.
Logging the context state changes show the stamps were added but the first was removed, so basically the first stamp has been overwritten.
You'd think this is simple, "just check you're dependency arrays". But this looks all okay to me.
Here's how my setup looks:
Context:
const AuthContextProvider = ({ children }: Props) => {
const [user, setUser] = useState<FirebaseAuthTypes.User>();
const [userData, setUserData] = useState<FirestoreUser>();
const [stamps, setStamps] = useState<GetMyStampsResponse>();
const [isLoading, setLoading] = useState(true);
// Unimportant for issue
const onAuthStateChanged = useCallback(
async (response: FirebaseAuthTypes.User | null) => {
...
},
[...]
);
useEffect(() => {
const unsubscribe = auth().onUserChanged(onAuthStateChanged);
return unsubscribe;
}, [onAuthStateChanged]);
return (
<AuthContext.Provider
value={{
isLoading,
stamps,
setStamps,
user,
userData,
reloadUser,
}}>
{children}
</AuthContext.Provider>
);
};
useAuth hook to be used in my components
import storage from '#react-native-firebase/storage';
import cloneDeep from 'lodash/cloneDeep';
import { useCallback, useContext } from 'react';
const useAuth = () => {
const { stamps, setStamps, reloadUser, userData, user, isLoading } =
useContext(AuthContext);
/**
* Add stamps that were received through a listener. This way we don't have to rely on reloadUser, which is more expensive.
*/
const addStampsFromListener = useCallback(
async (newStamps: FirestoreStamp[]) => {
let newStampsState = cloneDeep(stamps);
const newStampHasKnownStampcard = newStampsState?.cards.find(
c => c.ref.path === newStamps[0].stampcardRef.path,
);
console.log('current stamps state', newStampsState?.stamps.length);
if (newStampHasKnownStampcard) {
newStampsState = {
...newStampsState!,
stamps: newStampsState!.stamps.concat(newStamps),
};
} else {
const [stampcard, business] = await Promise.all([
stampcardApi.getStampcardByRef(newStamps[0].stampcardRef),
businessApi.getBusiness(newStamps[0].businessId),
]);
const imageUrl = await storage()
.ref(stampcard!.stampImage)
.getDownloadURL();
const stampcardWithRef: StampCardDataWithRef = {
...stampcard!,
imageUrl,
businessId: newStamps[0].businessId,
ref: newStamps[0].stampcardRef,
};
newStampsState = {
...newStampsState,
businesses: (newStampsState?.businesses ?? []).concat(business!),
cards: (newStampsState?.cards ?? []).concat(stampcardWithRef),
stamps: (newStampsState?.stamps ?? []).concat(newStamps),
};
}
setStamps(newStampsState);
return newStampsState;
},
[setStamps, stamps],
);
...
return {
addStampsFromListener,
updateStampsFromListener,
isLoading,
data: userData,
logout,
reloadUser,
stamps,
user,
};
};
And finally, the use of this context hook in my screen:
const HomeConsumerScreen = () => {
const {
stamps: stampcards,
reloadUser,
addStampsFromListener,
updateStampsFromListener,
} = useAuth();
const subscribeToStamps = useCallback(() => {
if (refIsSubscribedToStamps.current) return; // Prevent initialization when already running
const unsubscribe = stampApi.subscribeToStamps(
/**
* Callback for receiving new stamps
*/
querySnapshot => {
error && setError(undefined);
if (!refIsSubscribedToStamps.current) {
// First snapshot contains current state of all stamps belonging to a user
return;
}
const data = querySnapshot.map(doc =>
doc.doc.data(),
) as FirestoreStamp[];
addStampsFromListener(data).then(nextState =>
showDone('newStamp', data, nextState),
);
},
/**
* Initializer
*/
() => {
refIsSubscribedToStamps.current = true;
},
/**
* Error callback
*/
e => {
setError(e.message);
console.error('onSnapShot', e);
},
);
return unsubscribe;
}, [addStampsFromListener, error, showDone, updateStampsFromListener]);
useEffect(() => {
// Listen for changes to single stampcard
const unsubscribe = subscribeToStamps();
return () => {
unsubscribe && unsubscribe();
refIsSubscribedToStamps.current = false;
};
// Adding 'subscribeToStamps' to dependency hook will cause
// the listener to unmount&remount on every callback.
// It will fix the issue that I'm trying to solve here.... :(
}, []);
};
So it seems to revolve around adding subscribeToStamps to the useEffect dependency array, but that causes the listener to unmount&mount again which is just more costly.
Any advice?
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]);
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.
I have an Authentication Context that uses useEffect for getting data from sessionStorage and set a global user variable to pass down via context api.
On each protected route, I have a useEffect inside my hoc to check if the user is logged, and if it isn't send the user back to login page.
However, the useEffect inside the hoc is firing before the Authentication Context and therefore, doesn't see the authentication data and sends the customer to login page every time
const router = useRouter()
const [ user, setUser ] = useState(null)
const [ loading, setLoading ] = useState(false)
useEffect(() => {
async function loadUserFromSessionStorage() {
const token = sessionStorage.getItem('accessToken')
if (token) {
const { data: { customer: { name } } } = await axios.get(`http://localhost:3002/customer/token/${token}`)
if (name) setUser(name)
}
setLoading(false)
}
loadUserFromSessionStorage()
})
useEffect(() => {
if(!!user) router.push('/')
}, [ user ])
return (
<AuthContext.Provider
value={{ isAuthenticated: !!user, loading, user}}
>
{children}
</AuthContext.Provider>
)
}
And this is my HOC:
return () => {
const { user, isAuthenticated, loading } = useContext(AuthContext);
const router = useRouter();
useEffect(() => {
if (!isAuthenticated && !loading){
router.push("/login")
}
}, [ loading, isAuthenticated ]);
return (
isAuthenticated && <Component {...arguments} />
)
};
}
Does anyone know how to solve this?
As you may or may not know, the useEffect hook in your HOC will fire before the hook that loads your user from session state completes.
Where you went wrong is in setting your loading state to false by default:
const [ loading, setLoading ] = useState(false)
When you do this in the HOC effect hook
if (!isAuthenticated && !loading)...
That expression will be true the first time through and you get redirected to the login page. Just do useState(true) instead.
You're not passing the loading variable into the context, so when you deconstruct the context value in your HOC, it looks like:
{
user: undefined,
isAuthenticated: false,
loading: undefined
}
which, based on your logic will redirect to login.
Try adding the other two variables inside your AuthContext.Provider and see if that helps you out.
When a user arrives in this component. I want to execute two function in the useEffect hook:
- hasError (returns true if there is an error)
- hasState (returns true if there is an state)
Based on the state i want to return a specific component. If access is false than
If it is true than
The first time the user comes on the page it says that access if false when i refresh the page it is true Why doesn't the state update the first time?
Am I using the wrong lifecycle method?
Component:
const ContinueRouteComponent = () => {
const dispatch = useDispatch();
// const [access, setAccess] = useState(false);
// const [error, setError] = useState(false);
const query = useQuery();
//check if user has valid state
const hasState = (stateId: string) => {
// No state is given in the uri so no access
if(stateId === null) {
return false;
}
// You have a state in the uri but doesn't match the localstorage
return localStorage.getItem(stateId) !== null;
};
const hasError = (error: string) => {
return error !== null;
};
const [access, setAccess] = useState(() => hasState(query.get("state")));
const [error, setError] = useState(() => hasError(query.get("error")));
useEffect(() => {
dispatch(fetchResources());
},[]);
if(error){
let errorType = query.get("error");
let errorDescription = query.get("error_description");
return <Redirect exact={true} to={{
pathname: '/error',
state: {
error: errorType,
description: errorDescription
}
}} />;
}
if(access){
const retrievedStorage = JSON.parse(localStorage.getItem(query.get("state")));
localStorage.removeItem(query.get("state"));
return <Redirect exact to={retrievedStorage} />
} else {
return <Redirect to="/"/>
}
};
export default ContinueRouteComponent
On initial render access state is false
You set the default state as false, this will be its value on the first render. Then when you trigger the effect setAccess(hasState(someValue)); it sets up access to be true on the next render.
The change in state triggers a rerender with the new value but does not interrupt the current rendering.
You might spend more time thinking about yoursetup like, why not initialise the state with values?
const [access, setAccess] = useState(() => hasState(someValue));
const [error, setError] = useState(() => hasError(someValue));