Simplified code that I am using:
import { useState, useEffect, useContext } from 'react'
import { useRouter } from 'next/router'
import { firestore } from './firebase-config'
import { getDoc, doc } from 'firebase/firestore'
export default function HomePage() {
const router = useRouter()
const user = useContext(AuthContext) // contains user object -> user.user
const [loading, setLoading] = useState(true)
useEffect(() => {
const getData = async() => {
setLoading(true)
const uid = user.user.uid // uid of user in firebase auth
const id = router.query.id // id param of url
const docRef = doc(firestore, `...`)
// doc in a collection that references the above uid and id
const docSnap = await getDoc(docRef)
// get the document from firestore
if (docSnap.exists()) {
importData(docSnap.data()) // add data to store to re-render page
setLoading(false)
} else {
router.push('/main')
// if the user isn't logged in to return to '/'
// ^^ not sure how to do these separately
// if the user is logged in but the document does not exist to return to '/main'
}
}
getData()
}, [router.query, user.user])
return (
<>
{/* */}
</>
)
}
I need to load the document associated with the user's uid and the id param of the currently loaded page, i.e. /main/[id].
These retrieve a Firestore document that is then inserted into the store which causes the HomePage function to re-render to show the data.
uid is found in user.user.uid which is set via onAuthStateChanged in app.js
id is found in router.query.id which is set via useRouter() at the top level
The useEffect() above works, but only temporarily, soon after the data is loaded and the component re-renders, I am linked to '/main' as initially uid and id start as undefined meaning that on the first run of the useEffect hook the else condition is run, it then re-runs as the user and router object is retrieved to load the data, but by the time that has occurred the page is transitioned to './main'.
Would greatly appreciate some help to make this function work.
Additionally, the user should go back to './main' if the document doesn't exist but they are logged in, and if they are not logged in to then be returned to the root ('./')
Thanks in advance!
You can add a loading state for the document retrieval in addition to the loading state that you already have to make sure that the document retrieval is completed before navigating away from the page.
import { firestore } from './firebase-config'
import { getDoc, doc } from 'firebase/firestore'
export default function HomePage() {
const router = useRouter()
const user = useContext(AuthContext) // contains user object -> user.user
const [loading, setLoading] = useState(true)
const [docLoading, setDocLoading] = useState(true)
useEffect(() => {
const getData = async() => {
setLoading(true)
const uid = user.user.uid // uid of user in firebase auth
const id = router.query.id // id param of url
if (!uid) {
setLoading(false)
router.push('/')
return
}
if (!id) {
setLoading(false)
router.push('/main')
return
}
const docRef = doc(firestore, `...`)
// doc in a collection that references the above uid and id
setDocLoading(true)
const docSnap = await getDoc(docRef)
// get the document from firestore
setDocLoading(false)
if (docSnap.exists()) {
importData(docSnap.data()) // add data to store to re-render page
setLoading(false)
} else {
router.push('/main')
}
}
getData()
}, [router.query, user.user])
if (loading || docLoading) {
return <div>Loading...</div>
}
return (
<>
{/* render your component here */}
</>
)
}
So I have managed to fix the issue:
To be able to use uid in the useEffect() hook, onAuthStateChanged is called again rather than using the AuthContext that is created at the top level as this will wait until the user exists
To wait for router.query to be updated you can call router.isReady which returns a Boolean value on whether it has been updated.
Using both of these in this way:
useEffect(() => {
onAuthStateChanged(auth, async (user) => {
if (user) {
if (router.isReady) {
// do stuff -> user exists
} else {
// user exists but the document does not
router.push('/main')
}
}
else {
// user is not logged in
router.push('/')
}
})
}, [router.isReady, router.query])
Related
I'm using react-firebase-hook , and I'm trying to check if the user is admin or not, and I want to it to be a global state where I don't have to add this code in every and each component to check if the user is admin or not, here is the code..
import { useState, useEffect } from 'react';
import { query, collection, getDocs, where } from "firebase/firestore";
import { auth, db } from "../../config/fbConfig";
import { useAuthState } from "react-firebase-hooks/auth";
const CreateAnn = () => {
const [ann, setAnn] = useState(''); // ignore this
const [admin, setAdmin] = useState(false);
const [user] = useAuthState(auth);
const fetchAdmin = async () => {
try {
const q = query(collection(db, "users"), where("uid", "==", user?.uid));
const doc = await getDocs(q);
const data = doc.docs[0].data();
if(data.admin === true) {
setAdmin(true);
}
else { setAdmin(false); }
} catch (err) {
// do nothing
}
};
useEffect(() => {
fetchAdmin();
});
I want to have this as a global state, tried to useContext but i think I'm using it the wrong way, so anyone can help?
You are correct to use a context, however, you might use it wrong as you said.
You should set up a context that handles the currently logged in user.
In this context you can also fetch the extra details of the user from the user collection.
Also, you can grab the user directly with ID instead of where:
const docRef = doc(db, "users", user.uid);
const docSnap = await getDoc(docRef);
const data = docSnap.exists ? docSnap.data() : undefined
Follow this link to set up the context of auth correct.
https://dev.to/dchowitz/react-firebase-a-simple-context-based-authentication-provider-1ool
I am using Firebase Realtime Database for a site I am developing with React. In a useEffect method, I am using Firebase's get method to receive all the data from the database and it works when I switch from the home page back to the page I am displaying the data on but it doesn't work when I refresh my page. I have tried using an async await function, console.logging everything I could think of, and re-writing the entire code.
This is my useEffect method that fetches an input that was previously saved to the database. If I switch from the 'Journal' Router page to Home page and back, it loads correctly but it doesn't load correctly if I refresh the page. When I refresh, it console.logs 'No Data' but I know the data exists because when I switch between router pages it does load.
useEffect(() => {
const dbRef = ref(getDatabase())
//Fetches dreams from firebase's database
get(child(dbRef, `/${user.uid}/dreams`)).then(snapshot => {
if (snapshot.exists()){
const dreams = snapshot.val()
Object.values(dreams).forEach(dream => {
setUserDreams(prev => [...prev, dream])
})
} else {
console.log('No Data')
}
}).catch(err => {
console.error(err);
})
...
}, [])
The JSON structure of the database is basically this
"USER_ID" : {
"dreams" : [{"RANDOM_UUID" : {...}}],
"tags" : [{"RANDOM_UUID" : {...}}]
}
The user ID is the uid that firebase generates in their user authentication feature and it doesn't change and the random uuid is a random string generated from the firebase uuidv4 method.
This is how the user variable is populated:
import {createContext, useContext, useEffect, useState} from 'react'
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
updateProfile,
onAuthStateChanged
} from 'firebase/auth';
import { auth } from '../firebase-config';
const UserContext = createContext();
export const AuthContextProvider = ({children}) => {
const [user, setUser] = useState({})
const createUser = (email, password) => {
return createUserWithEmailAndPassword(auth, email, password);
}
const updateUsername = (username) => {
return updateProfile(auth.currentUser, {
displayName: username
})
}
const signIn = (email, password) => {
return signInWithEmailAndPassword(auth, email, password);
}
const logout = () => {
return signOut(auth);
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
console.log(currentUser)
setUser(currentUser)
})
return () => {
unsubscribe()
}
}, [])
return (
<UserContext.Provider value={{createUser, user, logout, signIn, updateUsername}}>
{children}
</UserContext.Provider>
)
}
export const UserAuth = () => {
return useContext(UserContext)
}
Sorry if this is a bit weird but I figured out the issue. After logging the user variable in my journal file, I learned that it isn't populated until after that useEffect is ran so I just put user as the dependency variable in my useEffect hook so it waits until it is populated to run that hook.
useEffect(() => {
const dbRef = ref(getDatabase())
//Fetches dreams from firebase's database
get(child(dbRef, `/${user.uid}/dreams`)).then(snapshot => {
if (snapshot.exists()){
const dreams = snapshot.val()
Object.values(dreams).forEach(dream => {
setUserDreams(prev => [...prev, dream])
})
} else {
console.log('No Data')
}
}).catch(err => {
console.error(err);
})
...
}, [user])
This is what worked, the only thing changed was the dependency array. Meaning, the user variable was populated after the useEffect hook ran which is what made me have issues. Thanks for the commenter that helped me out!
I have a session context for my NextJS application where anyone accessing /app/ directory pages have to go through an authorization check prior to allowing the user to access the page.
While my logic works in redirecting users without proper authentication, it is a bit glitchy because when someone navigate to the URL, /app/profile/ the page briefly loads before being redirected by Router.
I am wondering what is the best way to have this check happen prior to router loading the unauthorized page and redirecting them to the /login/ page.
Here are the steps in the authorization check:
Check is the user object has a property, authorized
Query the server for a session token
if the object from the server request comes back with authorized = false, then redirect user to /login/
Here is the code:
import React, { createContext, useContext, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import axios from 'axios'
export const SessionContext = createContext(null);
const AppSession = ({ children }) => {
const router = useRouter()
const routerPath = router.pathname;
const [user, setUser] = useState({ user_id: '', user_email: '', user_avatar: ''})
useEffect(()=> {
// Check for populated user state if pages are accessed with the path /app/
if (routerPath.includes("/app/")){
if (user){
if(user.authenticated === undefined){
// Check if user session exists
axios.get('/api/auth/session/')
.then(res => {
const data = res.data;
// Update user state depending on the data returned
setUser(data)
// If user session does not exist, redirect to /login/
if (data.authenticated === false){
router.push('/login/')
}
})
.catch(err => {
console.log(err)
});
}
}
}
}, [])
return (
<SessionContext.Provider value={{user, setUser}}>
{children}
</SessionContext.Provider>
)
}
export const getUserState = () => {
const { user } = useContext(SessionContext)
return user;
}
export const updateUserState = () => {
const { setUser } = useContext(SessionContext)
return (user) => {
setUser(user);
}
}
export default AppSession;
Since user.authenticated isn't defined in the initial user state you can conditionally render null or some loading indicator while user.authenticated is undefined. Once user.authenticated is defined the code should either redirect to "/login" or render the SessionContext.Provider component.
Example:
const AppSession = ({ children }) => {
const router = useRouter();
const routerPath = router.pathname;
const [user, setUser] = useState({ user_id: '', user_email: '', user_avatar: ''});
...
if (user.authenticated === undefined) {
return null; // or loading indicator/spinner/etc
}
return (
<SessionContext.Provider value={{ user, setUser }}>
{children}
</SessionContext.Provider>
);
};
Check out getServerSideProps, redirects in getServerSideProps and this article.
In your client-side, if you export the NextJS function definition named getServerSideProps from a page, NextJS pre-renders the page on each request using the data returned by getServerSideProps.
In other words, you can use getServerSideProps to retrieve and check the user while pre-rendering the page and then choose to redirect instead of render if your condition is not met.
Here is an example.
function Page({ data }) {
// Render data...
}
export async function getServerSideProps(context) {
const { req, res } = context;
try {
// get your user
if (user.authenticated === undefined) {
return {
redirect: {
permanent: false,
destination: `/`,
},
};
}
return {
props: {
// any static props you want to deliver to the component
},
};
} catch (e) {
console.error("uh oh");
return;
}
}
Good luck!
This seems like an easy one and that I'm just missing something obvious, but a little background:
I am making a mock "bug reporting" web app. The app has two types of user: "user" and "engineer". Upon signup the user is assigned a type, and it's saved in a firebase collection "users" under a firebase unique identifier "uid".
Upon login to the web app, a firebase user object for the logged in user is retrieved from firebase. This object has "user.uid" on it.
In order to grab the user type (saved as userType in the firebase document) I need to take that user.uid and send the uid part to a react hook I have made. This hook then fetches the document with that uid and returns the userType.
I've made the following dummy file to demonstrate.
import {React, useEffect, useState} from 'react'
import { useAuthContext } from '../../hooks/useAuthContext'
import { useDocument } from '../../hooks/useDocument'
export default function User() {
const { user } = useAuthContext();
console.log("uid: " + user.uid)
const id = user.uid;
console.log("id: " + id)
let { document, error } = useDocument("users", id)
console.log("userType: " + document.userType)
return (
<div>
</div>
)
}
Now the problem I have is that "user" isn't initalised from Context in time before the program tries to go fetch it, using a uid that, again, isn't initialised in time.
Basically I just need a way to delay using the useDocument hook, but I can't make hooks asynchronous. My async skills aren't my strongest point, to say the least.
Any help is massively appreciated, I've spent so many hours trying to crack this.
EDIT: Upon request, here is the useDocument hook:
import { useEffect, useState } from "react"
import { projectFirestore } from "../firebase/config"
export const useDocument = (collection, id) => {
const [document, setDocument] = useState(null)
const [error, setError] = useState(null)
// realtime document data
useEffect(() => {
const ref = projectFirestore.collection(collection).doc(id)
const unsubscribe = ref.onSnapshot(snapshot => {
// need to make sure the doc exists & has data
if(snapshot.data()) {
setDocument({...snapshot.data(), id: snapshot.id})
setError(null)
}
else {
setError('No such document exists')
}
}, err => {
console.log(err.message)
setError('failed to get document')
})
// unsubscribe on unmount
return () => unsubscribe()
}, [collection, id])
return { document, error }
}
I'm uploading a user profile image into firebase storage, then updating the photoURL value of the auth user with updateProfile(). After that I want the user to be updated without manually refreshing the page. I've been trying this for days now and the issue gets more weird every time I try to debug it.
The interesting thing is the user object seems to be already updated when I log it with console.log(currentUser) after the then promise of updateProfile() is fulfilled. So the new photoURL is already present in the currentUser object. But it seems to not call a state update or console.log("!!!!currentAuthUserUpdate", user);. So the user image wouldn't refresh in my page.
I even tried it with doing a useEffect with the currentUser as a dependency but it wasn't fired. Still, the currentUser object changed when logging it after updateProfile()
Updating the profile, UpdateUserImage.tsx:
import { useAuth } from "../../contexts/AuthContext";
const { currentUser } = useAuth();
// updating the user profile
updateProfile(currentUser, { photoURL })
AuthContext.tsx:
import { auth } from "./../firebase/firebase";
const [currentUser, setCurrentUser] = useState(null);
const auth = getAuth(app);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
console.log("!!!!currentAuthUserUpdate", user);
// I tried setting the user as a custom new object: const userData = { ...user };
setCurrentUser(user);
});
return unsubscribe;
}, []);
firebase.js
import { getAuth } from "firebase/auth";
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
export {
auth
};
What I tried additionally: (But this wouldn't work as the user is already updated without a state refresh from react, so it's trying to replace the same object)
const reloadUser = async () => {
try {
const res = await currentUser.reload();
const user = auth.currentUser;
console.log("currentUser:", user);
setCurrentUser(auth.currentUser);
console.log("res", res);
} catch (err) {
console.log(err);
}
};
it's not auth.onAuthStateChanged. You need to import onAuthStateChanged from 'firebase/auth'
import { getAuth, onAuthStateChanged } from "firebase/auth"
const auth = getAuth(); // leave getAuth empty if you only have one app
Then in your useEffect it should be
onAuthStateChanged(auth, async (currentUser) => { ... }
The setCurrentUser function returned from useState isn't always the same function in my experience.
You can try passing it as a dependency into the useEffect - but I don't think that's what you want.
React lets you use an old useState setter if you give it an updater function, rather than a value: setCurrentUser(()=>auth.currentUser)
The React docs dispute this though.
Using useStates are good for re-rendering components. However going into utilizing useRefs are best for updating the actual variable and will not cause a re-render of the component.
Declare it like:const currentUser = useRef(null)
Update it like: currentUser.current = updatedUserData
Use in code like: currentUser.current.photoURL