Supabase onAuthStateChange() triggers when switching tabs in React - reactjs

I have the following code in my React project using Supabase:
// supabaseClient.ts
export const onAuthStateChangedListener = (callback) => {
supabase.auth.onAuthStateChange(callback);
};
// inside user context
useEffect(() => {
const unsubscribe = onAuthStateChangedListener((event, session) => {
console.log(event);
});
return unsubscribe;
}, []);
However, every time I switch tabs away from the tab rendering the website to something else, and back, I see a new log from this listener, even if literally no change happened on the website.
Does anyone know the reason for this? The useEffect inside my user context component is the only place in my app where the listener is being called. To test, I wrote this dummy function inside my supabaseClient.ts file:
const testFunction = async () => {
supabase.auth.onAuthStateChange(() => {
console.log("auth state has changed");
});
};
testFunction()
This function also renders every time I switch tabs. This makes it a little annoying because my components that are related to userContext re render every time a tab is switched, so if a user is trying to update their profile data or something, they cannot switch tabs away in the middle of editing their data.

Supabase onAuthStateChange by default triggers every time a tab is switched. To prevent this, when initializing the client, add {multiTab: false} as a parameter.
Example:
const supabase = createClient(supabaseUrl, supabaseAnonKey, {multiTab: false,});

Here is my solution to the same problem. The way I've found is saving the access token value in a cookie every time the session changes, and retrieve it when onAuthStateChange get triggered, so I can decide to not update anything if the session access token is the same.
// >> Subscribe to auth state changes
useEffect(() => {
let subscription: Subscription
async function run() {
subscription = Supabase.auth.onAuthStateChange(async (event, newSession) => {
// get current token from manually saved cookie every time session changes
const currentAccessToken = await getCurrentAccessToken()
if (currentAccessToken != newSession?.access_token) {
console.log('<<< SUPABASE SESSION CHANGED >>>')
authStateChanged(event, newSession)
} else {
console.log('<<< SUPABASE SESSION NOT CHANGED >>>')
}
}).data.subscription
// ** Get the user's session on load
await me()
}
run()
return function cleanup() {
// will be called when the component unmounts
if (subscription) subscription.unsubscribe()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

Related

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

ReactJS delay update in useState from axios response

I am new to react js and I am having a hard time figuring out how to prevent delay updating of use state from axios response
Here's my code:
First, I declared countUsername as useState
const [countUsername, setUsername] = useState(0);
Second, I created arrow function checking if the username is still available
const checkUser = () => {
RestaurantDataService.checkUsername(user.username)
.then(response => {
setUsername(response.data.length);
})
.catch(e => {
console.log(e);
})
}
So, every time I check the value of countUsername, it has delay like if I trigger the button and run checkUser(), the latest response.data.length won't save.
Scenario if I console.log() countUseranme
I entered username1(not available), the value of countUsername is still 0 because it has default value of 0 then when I trigger the function once again, then that will just be the time that the value will be replaced.
const saveUser = () => {
checkUser();
console.log(countUsername);
}
Is there anything that I have forgot to consider? Thank you
usually there is a delay for every api call, so for that you can consider an state like below:
const [loading,toggleLoading] = useState(false)
beside that you can change arrow function to be async like below:
const checking = async ()=>{
toggleLoading(true);
const res = await RestaurantDataService.checkUsername(user.username);
setUsername(response.data.length);
toggleLoading(false);
}
in the above function you can toggle loading state for spceifing checking state and disable button during that or shwoing spinner in it:
<button onClick={checking } disabled={loading}>Go
i hope this help
.then is not synchronous, it's more of a callback and will get called later when the api finishes. So your console log actually goes first most of the time before the state actually saves. That's not really something you control.
You can do an async / await and return the data if you need to use it right away before the state changes. And I believe the way state works is that it happens after the execution:
"State Updates May Be Asynchronous" so you can't really control when to use it because you can't make it wait.
In my experience you use the data right away from the service and update the state or create a useEffect, i.g., useEffect(() => {}, [user]), to update the page with state.
const checkUser = async () => {
try {
return await RestaurantDataService.checkUsername(user.username);
} catch(e) {
console.log(e);
}
}
const saveUser = async () => {
const user = await checkUser();
// do whatever you want with user
console.log(user);
}

How to save and then later delete timeoutId in React inside a async thunk that is dispatched

I have a problem that I don't know how to fix.
I want to call refresh-token endpoint when setTimeout time runs out. setTimeout is called in 2 different ways:
When user logs in
When user enters a page again and has bearer token
If I just called setTimeout without deleting the previous one, a user could log in and out and log in again and it would send a request to the refresh-token endpoint for every setTimeout that was created (in this case, 2 requests).
What I want to do is save the setTimeout that was created and if user decides to log out and then log in again, we delete setTimeout and create a new one. However, when I store my timeoutId with useState, I get Invalid hook call error.
This is how my code looks like:
storageActions.ts
export function setRefreshTokenTimeout(bearerToken: string, refreshToken: string) {
removeRefreshTokenTimeout();
var decodedBearerToken: any = jwt_decode(bearerToken);
let remainingDuration = calculateRemainingTime(decodedBearerToken.exp * 1000);
remainingDuration = 10000;
const refreshTokens = setTimeout(() => store.dispatch(refreshTokensThunk({ refreshToken })), remainingDuration);
setRefreshTokensTimeout(refreshTokens);
}
export function removeRefreshTokenTimeout() {
if (refreshTokensTimeout) {
clearTimeout(refreshTokensTimeout);
setRefreshTokensTimeout(undefined);
}
}
authActions.ts
export const userLoginThunk = createAsyncThunk<UserLoginResponse, UserLoginRequest>(
"auth/user-login",
async (request, thunkAPI) => {
let response = await AuthServiceUserLogin(request);
setUserLocalStorageData(response.data);
setRefreshTokenTimeout(response.data.bearerToken, response.data.refreshToken);
return response.data;
}
);
export const authUserThunk = createAsyncThunk("auth/authUser", async thunkAPI => {
await AuthServiceAuthUser();
const userStorage = getUserLocalStorageData();
if (userStorage?.data?.bearerToken) {
setRefreshTokenTimeout(userStorage.data.bearerToken, userStorage.data.refreshToken);
}
});
All of these are called inside a createAsyncThunk that is dispatched. Is there any way to solve this problem?
I knew this was because of useState hook, but I thought that you can only save states with hooks like useState or useRef.
I just figured out that this rule only refers to React components. storageActions.ts isn't a component, therefore I can use a simple variable to save my timeoutId.
I tested it and it works.

Why is my React UseEffect (that uses Firebase onSnapshot) in an infinite loop, constantly mounting and unmounting?

I have a header (like facebook I suppose) and there is a notifications bell. I want to listen for new notifications on Firbase Firestore.
I've set up a useEffect, but theres an infinite loop for some reason, and I'm not sure why?
Here is the useEffect/useState:
const [subbedToNotifications, setSubbedToNotifications] = useState(false);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
useEffect(() => {
var unsubscribe = () => {
console.log("this function does nothing");
};
console.log("subscribing to notifications");
if (
userData !== null &&
auth.currentUser.uid !== null &&
!subbedToNotifications
) {
setSubbedToNotifications(true);
unsubscribe = db
.collection("users")
.doc(auth.currentUser.uid)
.onSnapshot((snapshot) => {
dispatch({
type: "SET_USERDATA",
userData: snapshot.data(),
});
});
}
// Detach listener
return () => {
console.log("unsubbing from notifications");
unsubscribe();
setSubbedToNotifications(false);
};
}, [subbedToNotifications]);
So, when the component mounts and whenever subbedToNotifications changes, it'll run this use effect. The IF wrapped around the call to Firebase requires userData (where I store things such as username, profile picture, bio etc.), firebase auth to not be empty, as well as subbedToNotifications to be false. When the call to firebase is made, subbedToNotifications is set to true, so Therefore that shouldn't be called again and should only be subscribed once.
Also the return unsubscribes from firebase, however the console logs were repeating:
console.log("subscribing to notifications") and console.log("unsubbing from notifications") constantly, racking up 6k reads to the database.
Can anyone spot my mistake as to why the useEffect is getting into a loop, or perhaps why the Header is constantly mounting and unmounting?
TIA!
Try changing [subbedToNotifications] to []

Function inside useEffect fire twice even with empty dependency array

I have this example from https://github.com/vercel/next.js/blob/canary/examples/with-firebase-authentication/utils/auth/useUser.js
The effect works fine (fires once) but for some reason, the functions inside are called twice.
useEffect(() => {
const cancelAuthListener = firebase
.auth()
.onIdTokenChanged(async (user) => {
console.log('once or twice?')
if (user) {
// This fires twice
const userData = await mapUserData(user)
setUserCookie(userData)
setUser(userData)
} else {
removeUserCookie()
setUser()
}
})
const userFromCookie = getUserFromCookie()
if (!userFromCookie) {
router.push('/')
return
}
setUser(userFromCookie)
console.log(' i fire once')
return () => {
console.log('clean up')
cancelAuthListener()
}
}, [])
How can I make it to fire once?
I added some console logs:
On the first render I get: 'i fire once', 'once or twice', 'once or twice'
If I leave the page the cleanup console log fires (as it's supposed to do)
Many thanks
Later edit:
this is the code
export const mapUserData = async (user) => {
const { uid, email } = user
const token = await user.getIdToken()
return {
id: uid,
email,
token
}
}
If getIdToken() gets 'true' as an argument it will force a refresh regardless of token expiration.
https://firebase.google.com/docs/reference/js/firebase.User#getidtoken
Solved!!
the user was calling getIdToken(true) which forces a refresh.
https://firebase.google.com/docs/reference/js/firebase.User#getidtoken
Sorry guys, my bad!!!
You have a setState() inside useEffect thats the culprit, where useEffect having empty params [], one request on initial mount and another when do
setUser(userData) the component re-renders and useEffect() is invoked again.
Instead of using user as state, try using as ref and check. That might resolve this.

Resources