I want to have my users log in automatically anonymously. That's not too difficult to do. However I don't want anonymous logins to override their account logins. That's where I am running into trouble. I can't seem to find the way to do this.
Here is my hook:
import React, { useEffect, useState } from 'react';
import { singletonHook } from 'react-singleton-hook';
import { useAuth, useUser } from 'reactfire';
function SignInAnonymously() {
const auth = useAuth();
const user = useUser();
useEffect(() => {
user.firstValuePromise.then(() => {
if (!user.data) {
auth.signInAnonymously().then(() => {
console.log('Signed in anonymously');
}).catch((e) => console.log(e));
}
});
}, [user.firstValuePromise, user.data]);
return <></>;
}
export default singletonHook(<></>, SignInAnonymously);
The idea is that we get the first value emitted and compare that to the data object. However, it does not work as I would expect. The value emitted even for someone that was signed in returns null. If I comment the hook the user stays logged into their account. I have spent hours on this trying all the properties of the user so any help is appreciated.
Inside the useUser() method of reactfire, they use firebase.auth().currentUser as the initial value of the observable as seen on this line.
As covered in the Firebase Authentication docs:
Note: currentUser might also be null because the auth object has not finished initializing. If you use an observer to keep track of the user's sign-in status, you don't need to handle this case.
By reactfire setting the initial value to currentUser, you will often incorrectly get the first value as null (which means firstValuePromise will also resolve as null) because Firebase Auth hasn't finished initializing yet.
To suppress this behaviour, we need specify a value for initialData to pass in to useUser. I'd love to be able to use undefined, but thanks to this truthy check, we can't do that. So we need some truthy value that we can ignore such as "loading".
Applying this to your component gives:
/**
* A truthy value to use as the initial value for `user` when
* `reactfire` incorrectly tries to set it to a `null` value
* from a still-initializing `auth.currentUser` value.
*
* #see https://stackoverflow.com/questions/67683276
*/
const USER_LOADING_PLACEHOLDER = "loading";
function SignInAnonymously() {
const auth = useAuth();
const user = useUser({ initialData: USER_LOADING_PLACEHOLDER });
useEffect(() => {
if (user.data !== null)
return; // is still loading and/or already signed in
auth.signInAnonymously()
.then(() => console.log('Signed in anonymously'))
.catch((e) => console.error('Anonymous sign in failed: ', e));
}, [user.data]);
return <></>;
}
Related
I'm trying to implement authentication in my app using Firebase and I need to store some custom user fields (e.g. schoolName, programType, etc.) on the user documents that I'm storing in Firestore. I want to have these custom fields in my React state (I'm using Recoil for state management), and I'm very unsure of the best way to do this.
I currently have a Cloud Function responsible for creating a new user document when new auth users are created, which is great, however, I'm having trouble figuring out a good way to get that new user (with the custom fields) into my state, so I came up with a solution but I'm not sure if it's ideal and would love some feedback:
I define the firebase/auth functions (e.g. signInWithPopup, logout, etc.) in an external static file and simply import them in my login/signup forms.
To manage the user state, I created a custom hook useAuth:
const useAuth = () => {
const [user] = useAuthState(auth); // firebase auth state
const [currentUser, setCurrentUser] = useRecoilState(userState); // global recoil state
useEffect(() => {
// User has logged out; firebase auth state has been cleared; so clear app state
if (!user?.uid && currentUser) {
return setCurrentUser(null);
}
const userDoc = doc(firestore, "users", user?.uid as string);
const unsubscribe = onSnapshot(userDoc, (doc) => {
console.log("CURRENT DATA", doc.data());
if (!doc.data()) return;
setCurrentUser(doc.data() as any);
});
if (currentUser) {
console.log("WE ARE UNSUBBING FROM LISTENER");
unsubscribe();
}
return () => unsubscribe();
}, [user, currentUser]);
};
This hook uses react-firebase-hooks and attempts to handle all cases of the authentication process:
New users
Existing users
Persisting user login on refresh (the part that makes this most complicated - I think)
To summarize the above hook, it essentially listens to changes in firebase auth state via useAuthState, then I add a useEffect which creates a listener of the user document in firestore, and when that user has successfully been inputted into the db by the Cloud Function, the listener will fire, and it will populate recoil state with doc.data() (which contains the custom fields) via setCurrentUser. As for existing users, the document will already exist, so a single snapshot will do the trick. The rationale behind the listener is the case of new users, where a second snapshot will be required as the first doc.data() will be undefined even though useAuthState will have a user in it, so it's essentially just waiting for the Cloud Function to finish.
I call this hook immediately as the app renders to check for a Firebase Auth user in order to persist login on refresh/revisit.
I've been messing around on this for quite some time, and this outlined solution does work, but I have come up with multiple solutions so I would love some guidance.
Thank you very much for reading.
Step 1: Define CurrentUser, and UserProfile states
import { atom, selector } from "recoil";
import { type User } from "firebase/auth";
export const CurrentUser = atom<User | null | undefined>({
key: "CurrentUser",
dangerouslyAllowMutability: true,
defaultValue: undefined,
});
export const UserProfile = atomFamily<Profile | null, string | undefined>({
key: "CurrentUser",
dangerouslyAllowMutability: true,
get(uid) {
return undefined;
}
});
Step 2: Listen to the authenticated user state changes
export const CurrentUser = atom<User | null | undefined>({
key: "CurrentUser",
dangerouslyAllowMutability: true,
defaultValue: undefined,
effects: [
(ctx) => {
if (ctx.trigger === "get") {
// Import Firebase App instanced defined in a separate chunk
const promise = import("../core/firebase")
.then((fb) =>
fb.auth.onAuthStateChanged((user) => {
ctx.setSelf(user);
})
)
.catch((err) => ctx.setSelf(Promise.reject(err)));
return () => promise.then((unsubscribe) => unsubscribe?.());
}
},
],
});
Step 3: Load user profile by Firebase user UID
export const UserProfile = atomFamily<User | null | undefined, string | undefined>({
key: "CurrentUser",
dangerouslyAllowMutability: true,
get(uid) {
return async function() {
if (!uid) return null;
import("../core/firebase").then(({ fs }) => {
// TODO: Retrieve Firestore document with the user profile
return getDoc(doc(dollection(fs, "users"), uid));
});
};
}
});
Step 4: Add React hooks
import { useRecoilValue } from "recoil";
export function useCurrentUser() {
return useRecoilValue(CurrentUser);
}
export function useCurrentUserProfile() {
const me = useRecoilValue(CurrentUser);
return useRecoilValue(UserProfile(me?.uid));
}
Usage Example
import { useCurrentUser, useCurrentUserProfile } from "../state/firebase";
export function Example(): JSX.Element {
const me = useCurrentUser(); // Firebase user object
const profile = useCurrentUserProfile(); // Custom profile from Firestore
}
See https://github.com/kriasoft/cloudflare-starter-kit for a working example
Every time I reload the my account page, it will go to the log in page for a while and will directed to the Logged in Homepage. How can I stay on the same even after refreshing the page?
I'm just practicing reactjs and I think this is the code that's causing this redirecting to log-in then to home
//if the currentUser is signed in in the application
export const getCurrentUser = () => {
return new Promise((resolve, reject) => {
const unsubscribe = auth.onAuthStateChanged(userAuth => {
unsubscribe();
resolve(userAuth); //this tell us if the user is signed in with the application or not
}, reject);
})
};
.....
import {useEffect} from 'react';
import { useSelector } from 'react-redux';
const mapState = ({ user }) => ({
currentUser: user.currentUser
});
//custom hook
const useAuth = props => {
//get that value, if the current user is null, meaning the user is not logged in
// if they want to access the page, they need to be redirected in a way to log in
const { currentUser } = useSelector(mapState);
useEffect(() => {
//checks if the current user is null
if(!currentUser){
//redirect the user to the log in page
//we have access to history because of withRoute in withAuth.js
props.history.push('/login');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},[currentUser]); //whenever currentUser changes, it will run this code
return currentUser;
};
export default useAuth;
You can make use of local storage as previously mentioned in the comments:
When user logs in
localStorage.setItem('currentUserLogged', true);
And before if(!currentUser)
var currentUser = localStorage.getItem('currentUserLogged');
Please have a look into the following example
Otherwise I recommend you to take a look into Redux Subscribers where you can persist states like so:
store.subscribe(() => {
// store state
})
There are two ways through which you can authenticate your application by using local storage.
The first one is :
set a token value in local storage at the time of logging into your application
localStorage.setItem("auth_token", "any random generated token or any value");
you can use the componentDidMount() method. This method runs on the mounting of any component. you can check here if the value stored in local storage is present or not if it is present it means the user is logged in and can access your page and if not you can redirect the user to the login page.
componentDidMount = () => { if(!localStorage.getItem("auth_token")){ // redirect to login page } }
The second option to authenticate your application by making guards. You can create auth-guards and integrate those guards on your routes. Those guards will check the requirement before rendering each route. It will make your code clean and you do not need to put auth check on every component like the first option.
There are many other ways too for eg. if you are using redux you can use Persist storage or redux store for storing the value but more secure and easy to use.
I have got an issue where I need to update the name in my useContext and then straight away check it against another part of my code. Although when I check it against another part the name value in the context is still empty.
So this is my context file...
export const UserContext = createContext();
function UserContextProvider(props) {
const [user, setUser] = useState({
name: '',
email: ''
});
function updateUser(field, value) {
return new Promise((resolve, reject) => {
setUser((prevValue) => {
return {...prevValue, [field]: value}
})
resolve();
})
}
return (
<UserContext.Provider value={{user, updateUser}}>
{props.children}
</UserContext.Provider>
)
}
And this is what I want to run...
const { user, updateUser } = useContext(UserContext);
async function method(event) {
event.preventDefault();
await updateUser("name", "Shaun")
console.log(user);
}
I also tried with async and await but that didn't work either
I have re-produce the issue in my codesandbox here: https://codesandbox.io/s/youthful-smoke-fgwlr?file=/src/userContext.js
await in await updateUser("name", "Shaun") is just wait for the Promise at return new Promise((resolve, reject) didn't await for setUser, setUser is asynchronous function (not a Promise). So if you want to check get latest user you need to check it in another useEffect in App.js
useEffect(() => {
console.log(user);
}, [user]);
Simply put: you can't do it the way you think. Note that user is a const. Calling setUser within updateUser does not change the fact that user is still bound to the same value it was before. At no point during this execution of your component's render function is user being re-assigned (and if it were, you'd get an error because it is a const). user will not change until the next time your component is refreshed and useState is called again (which will be very shortly, considering you just called setUser).
That said, I'm not sure what use case would actually make sense to check it after calling setUser. You already know that the user's name should be Shaun, so if you need that value for something, you already have it; checking that it has been assigned to user isn't necessary or useful at all. If you expect your updateUser function to potentially make changes to the value you pass, and you need to validate that the value is correct, you should have your async function return the value. Then you can check that value after the promise resolves. That said, any check like that probably should exist within your updateUser function anyways; any attempt to check or validate it after calling setUser seems like an anti-pattern.
i have tried to do routing according to user check and i did but my method worked two times. i understand it from my log
i'm using redux and here is my "useeffect" in mainrouter.js
// currentUser : null // in initialState for users
useEffect(() => {
if(states.currentUser== null)
{
console.log("Checking user for routing");
actions.getCurrentUserACT();
}
else{
console.log("Logged in")
}
},[]);
my mainrouter.js return
return (
<NavigationContainer>
{
states.currentUser == null ?
(this.SignStackScreen()) :
(this.MainTabNavigator())
}
</NavigationContainer>
);
here is my "getCurrentUserAct" method in redux actions
export function getCurrentUserACT() {
return function (dispatch) {
firebase.auth().onAuthStateChanged(user => {
if (user) {
console.log("User authed..");
dispatch({type:actionTypes.CURRENT_USER,payload:user})
} else {
console.log("User notauthed..");
dispatch({type:actionTypes.CURRENT_USER,payload:null})
}
});
}
when my app start, logs are .. (if i logged in before)
Checking user for routing
User authed
User authed
i think the reason for this, useeffect worked first time and state changed which use in return and rerender"
but then i removed to condition, logs were same.
so how i can avoid this?
is there a way to run method before from useeffect and avoid second render? or wait data in useeffect?
Your code looks right and there is no reason to log two times because of useEffect because you used it as componentDidMount so it is called one time.
The reason of logging two times is this code snippet:
firebase.auth().onAuthStateChanged(user => {
here onAuthStateChanged is callback and it is called when state of authentication changed on Firebase.
If you are going to check if the user logged in then you need to avoid to do like below
if (user) { }
Please try console.log(user) and check if user keeps same on double logging.
I believe user inner properties will be changed.
Hope this helps you to understand
export function getCurrentUserACT() {
return function (dispatch) {
firebase.auth().onAuthStateChanged(user => {
dispatch(setCurrentUserACT(user))
console.log("control point" + user);
// if (user) {
// console.log("User authed..");
// dispatch({type:actionTypes.CURRENT_USER,payload:user})
// } else {
// console.log("User notauthed..");
// dispatch({type:actionTypes.CURRENT_USER,payload:null})
// }
});
}
Logs were double
Checking user for routing
control point[object Object]
control point[object Object]
______________________Edited 31.05______________________
I change my code a little bit like below
const dispatch = useDispatch();
const currentUser = useSelector(state => state.currentUserReducer);
useEffect(() => {
console.log(JSON.stringify(currentUser));
if (currentUser == null) {
console.log("Checking user for routing");
//dispatch(getCurrentUserACT());
}
else {
console.log("Logged in")
}
}, []);
I found something like hint but I can't fix because i dont know
When app start logs are
null
Checking user for routing
I didnt dispatch getCurrentUserAct(), so logs are right. If i dispatch, user will be checking (user already logged in), component rerender and here is double logs from double render.
I need dispatch before useeffect, not in useeffect
Is there a way?
I need to fetch the user data and display it. I am getting an error now that says
TypeError: this.unsubscribe is not a function
and when I initialise it as a normal variable like const db, then I get another error
Function CollectionReference.doc() requires its first argument to be of type non-empty string
import React from "react";
import { auth, firestore } from "../../firebase/firebase.utils";
export default class UserPage extends React.Component {
state = {
user: {}
};
unsubscribe = null;
componentDidMount() {
const user = auth.currentUser;
this.unsubscribe = firestore
.collection("users")
.doc(user)
.onSnapshot(doc => {
this.setState = {
user: doc.data()
};
});
}
componentWillMount() {
this.unsubscribe();
}
render() {
return (
<div>
<h1>{this.state.user.name}</h1>
</div>
);
}
}
when I insinalise it as a normal variable like const db
Not quite sure what you mean by this, but if you're getting an error about the type of unsubscribe, I suggest using console.log right before you call it to view its value.
Bear in mind that componentWillMount happens in the lifecycle before componentDidMount (hence the names will and did). I suspect that's one of your problems: you try to call unsubscribe before setting the value.
With regard to your other error about the doc call, it's likely referring to:
...
.collection("users")
.doc(user) <-- this line
.onSnapshot(doc => {
this.setState = {
us
...
As the error output states, that user variable (the first argument of doc) must be a string, and it can't be an empty string.
I don't see user anywhere in your code, so I expect that it's currently the value undefined. You could access this.state.user here, but I'd strongly advise against it since you subsequently set that state in the call (probably cause an infinite loop).
What is your end goal? What have you tried to resolve these two issues? Maybe adding that to your question would help us assist you better.