Next.js production: CANNOT READ DATA FROM FIRESTORE - reactjs

I have a next.js application in which I have a real-time chat feature.
A get() request is supposed to be sent to firestore to check if chat between user A and user B already exists. If it does not already exist, a new chat between A and B is created
Problem:
The get() request to firestore always returns an empty value. So a new chat room between user A and user B is always created.
Since a new chat room is created successfully, I can write to firestore but cannot read data.
the code:
import { useEffect, useState } from "react";
import { dbs } from "../utils/firebase";
function Test() {
const [chats, setChats] = useState([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await dbs
.collection("chats")
.where("users", "array-contains", "some#email")
.get();
setChats(response);
} catch (err) {
console.log(err);
}
};
fetchData();
}, []);
console.log("chat is", chats);
return <div></div>;
}
export default Test;
What I have attempted and discovered:
I deployed an entirely new next.js app (let's call it app B) and tried to get data from the same firestore project with the same code above and it works. So my firestore configuration is fine, the code is fine, and there's nothing wrong with the deployment server.
Since there's nothing wrong with firestore or the deployment server or the code, I think something is wrong with the next.js app itself and the dependencies surrounding it. I don't know why it works in app B and not in app A.
Note, in App A:
Everything works fine in development mode, I can get the data.
I'm also using mongoDB and other third-party APIs in this app and I can successfully read and write data to these APIs. Only the get() request to firestore is not working. which made me think the problem must be with my firestore config but app B proves that's not the case.
I'm using google OAuth with mongoDB and next-auth to authenticate my users in app A. In app B I'm only using google OAuth without the DB.
I could show the code for both app A and app B but they are exactly the same. The code under the _app.js file is the same, the way I structure it is also the same. In both app A and app B, I called the get() request to firestore from a Test.js component in a test.js page but the get() request only returns value in app B and always returns null in app A.
So basically the only difference is app A is an actual app with plenty of dependencies, libraries, and files. app B is just a test app.
My question now is:
Could the read operation to firestore be affected by other dependencies or npm libraries in the project? or could it be affected by using another DB to get the auth session context?
Why is the write operation successful but not the read?
Why does it work in dev mode but not in prod mode? is there any difference between retrieving data from firestore in dev mode vs prod mode?

As explained yesterday, you aren't paying close enough attention to the intermediate states properly and it is likely that NextJS is tripping up because of it.
On the first render of your current code, chats is an empty array. Even so, once the promise fulfills, you update chats to a QuerySnapshot object, not an array.
import { useEffect, useState } from "react";
import { dbs } from "../utils/firebase";
function Test() {
const currentUserEmail = "some#email"; // get from wherever
const [chatsInfo, setChatsInfo] = useState({ status: "loading", data: null, error: null });
useEffect(() => {
let unsubscribed = false;
const fetchData = async () => {
return dbs
.collection("chats")
.where("users", "array-contains", currentUserEmail)
.get();
};
fetchData()
.then(() => {
if (unsubscribed) return; // do nothing
setChatsInfo({
status: "loaded",
data: response.docs, // QueryDocumentSnapshot[]
error: null
});
})
.catch((err) => {
if (unsubscribed) return; // do nothing
setChatsInfo({
status: "error",
data: null,
error: err
});
});
};
// return a unsubcribe callback that makes sure setChatsInfo
// isn't called when the component is unmounted or is out of date ​
​return () => unsubscribed = true;
​ }, [currentUserEmail]); // rerun if user email changes
​
​ const { status, data: chats, error } = chatsInfo;
console.log(JSON.parse(JSON.stringify({ status, chats, error }))); // crude object copy
switch (status) {
case "loading":
return (<div>Loading...</div>);
case "error":
return (
<div class="error">
Failed to load data: {error.code || error.message}
</div>
);
}
// if here, status is "loaded"
if (chats.length === 0) {
// You could also show a "Message Someone" call-to-action button here
return (
<div class="error">
No chat groups found.
</div>
);
}
// stamp out list of chats
return (<>
{chats.map((chatDoc) => {
return (
<div key={chatDoc.id}>
{JSON.stringify(chatDoc.data())}
</div>
);
})}
</>);
}
export default Test;
Notes:
A decent chunk of the above code can be eliminated by using an implementation of useAsyncEffect like #react-hook/async and use-async-effect. These will handle the intermediate states for you like loading, improper authentication, unmounting before finishing, and other errors (which are all not covered in the above snippet). This thread contains more details on this topic.
I highly recommend not using email addresses in their raw form for user IDs. With the current structure of your database, anyone can come along and rip all the emails out and start spamming them.
Each user should have some private identifier that doesn't reveal sensitive information about that user. This could be a Firebase Authentication User ID, the user's email address hashed using md5 (which also allows you to use Gravatar for user profile pictures) or some other ID like a username. Once you have such a user ID, you can use the approach outlined in this thread for handling messages between users.

Related

How to manage global states with React Query

i have a project that's using Nextjs and Supabase. I was using context API and now i'm trying to replace it for React Query, but i'm having a hard time doing it. First of all, can i replace context completely by React Query?
I created this hook to get the current user
export const getUser = async (): Promise<Profile> => {
const onFetch = await supabase.auth.getUser();
const userId = onFetch.data.user?.id;
let { data, error } = await supabase
.from("profiles")
.select()
.eq("id", userId)
.single();
return data;
};
export const useUser = () => {
return useQuery(["user"], () => getUser());
};
I'm not sure how to trigger it. When i was using context i was getting the user as this. If data, then it would redirect to the HomePage
let { data, error, status } = await supabase
.from("profiles")
.select()
.eq("id", id)
.single();
if (data) {
setUser(data);
return true;
}
Since i was getting the user before redirecting to any page, when i navigated to profile page, the user was already defined. How can i get the user before anything and keep this state? I suppose that once the user is already defined, at the profile component i can call useUser and just use it's data. But it's giving me undefined when i navigate to profile, i suppose that it's fetching again.
const { data, isLoading } = useUser();
But it's giving me undefined when i navigate to profile, i suppose that it's fetching again.
Once data is fetched when you call useUser, it will not be removed anymore (unless it can be garbage collected after it has been unused for some time). So if you do a client side navigation (that is not a full page reload) to another route, and you call useUser there again, you should get data back immediately, potentially with a background refetch, depending on your staleTime setting).
If you're still getting undefined, one likely error is that you are creating your QueryClient inside your app and it thus gets re-created, throwing the previous cache away. You're not showing how you do that so it's hard to say. Maybe have a look at these FAQs: https://tkdodo.eu/blog/react-query-fa-qs#2-the-queryclient-is-not-stable

Firebase auth causes error when trying to validate users premium status via custom claims offline

I have an app that I want premium users to be able to access a "pro dashboard".
This is how I currently get the premium user:
export default async function isUserPremium(): Promise<boolean> {
await auth.currentUser?.getIdToken(true);
const decodedToken = await auth.currentUser?.getIdTokenResult();
return decodedToken?.claims?.stripeRole ? true : false;
}
export default function usePremiumStatus(
user: UseQueryResult<User | null, AuthError>
) {
const [premiumStatus, setPremiumStatus] = useState<boolean>(false);
useEffect(() => {
if (user) {
const checkPremiumStatus = async function () {
setPremiumStatus(await isUserPremium());
};
checkPremiumStatus();
}
}, [user]);
return premiumStatus;
}
And this is how I get the status in my react component:
const isPremium = usePremiumStatus(user);
2 issues:
The state, doesn't persist and whenever I mount my component, even premium users will see basic dashboard screen for a split second, due to conditional rendering.
Everytime, I go offline, I get this error message that crashes the app.
What can I do to prevent this from happening? Or should I store users premium status in firestore database and if user goes offline, the app would use cached data, but in that case if users won't refresh the page, how can I ensure that the clients premium status is synced and is always updated with the most recent firestore data?

NextJS / React - how to avoid multiple requests to server?

I'm writing a little side project in which I have a question about how to design my frontend authorization flow.
Stack is: Next.js / Express / MDB
Here's the issue with the authorization: In my app there are pages that should be only served to instructors.
When the app mounts I have a call to my backend checking for a stored cookie and authorize the user. The returned user is stored in a Context.
authContext.js
useEffect(() => {
checkUserLoggedIn();
}, []);
...
const checkUserLoggedIn = async () => {
const res = await fetch(`${process.env.NEXT_PUBLIC_CLIENT}/user`);
const data = await res.json();
if (res.status === 200) {
setUser(data.user);
} else {
setUser(null);
}
};
That works perfectly fine.
But now comes the part where I struggle.
The protected pages are wrapped by a component that checks again if the user is a) authenticated and b) authorized.
wrapInstructor.js
const CheckInstructor = ({ token, children }) => {
const [ok, setOk] = useState(false);
const [login, setLogin] = useState(null);
useEffect(async () => {
try {
const user = await fetch(
`${process.env.NEXT_PUBLIC_APIURL}/me`,
{
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const { data, error } = await user.json();
if (error) {
toast.error("Auf diese Seite können nur eingeloggte User zugreifen");
setLogin(false);
}
if (data && data.role.includes("INSTRUCTOR")) {
setOk(true);
setLogin(true);
}
} catch (error) {
toast.error(error);
setLogin(false);
}
}, []);
return !ok ? (
<>{login === false ? <NotLoggedIn /> : <Spinner />}</>
) : (
<>{children}</>
);
};
export default CheckInstructor;
Now here comes the problem:
When a user mounts the app on a path that is protected the app fires off two authentication requests simultaniously.
The first one gets a valid response (the request from context). The second one from the wrapper gets a 304 status code which indicates that the response has not changed since the last request.
But since the wrapper expects a valid response the content of the protected pages will not show.
How can I cope with this problem? (NOTE: I'm in development mode, tested from localhost)
My ideas were:
The wrapper component does not make another call to the server - the user in the context is the single source of truth and the wrapper only checks the already stored user --> Question: Is this a safe practice? Can the store (React Context or Redux) be manipulated by the user which would make my app unsafe?
In the wrapper component also show the children components if the status code is 304 AND a user is already stored in the context
Write another endpoint in the backend so both requests are sent to different routes (I would concider this bad practice because DRY)
I'm a bit lost here - can you help me to clear my thoughts?
Thanks a lot!
There's no need to visit the same endpoint twice to check if a user is authorized to visit a certain page. What you store in auth context would be enough. Just make sure to update the value whenever a role or permissions change.
Regarding your security concern, you shouldn't consider any client app safe. Even if you add an actual endpoint call on every protected page, there's still a way to call the endpoints directly(curl, postman, you name it).
The problem should be solved by introducing authorization checks on every protected API route. This way you would never worry about corrupted clients at all.

"Failed to get document because the client is offline" Firebase error with the firestore emulator

EDIT: I know it is something with the emulators as it works fine without them.
I am following a course for Next js and i am using the Firebase emulator (it recommends to do so but doesn't have a tutorial to do so as its very easy) and I am trying to read some data from firestore for a user, but it always gets the error, Failed to get document because the client is offline. The auth emulator works fine but the firestore one doesn't. Even if I was disconnected from the internet (which i am not as I am writing this question and can visit any website) idk why it would have that error as the website is being hosted on next js. Here is the custom hook I made in the course to connect to firebase:
import { initializeApp } from "firebase/app";
import {
getAuth,
signInWithPopup,
signOut,
signInAnonymously,
GoogleAuthProvider,
connectAuthEmulator,
} from "firebase/auth";
import { getFirestore, doc, onSnapshot, connectFirestoreEmulator, getDoc } from "firebase/firestore";
const firebaseConfig = {
apiKey: "MY APIKEY",
authDomain: "My AUTH DOMAIN",
projectId: "nextfirE (in the course i am using we are making are making a blog type app called nextfire)",
storageBucket: "STOREAGEBUKET DOMAIN",
messagingSenderId: "messagingSenderId",
appId: "aPP id",
measurementId: "measurementId",
};
const firebase = initializeApp(firebaseConfig);
const auth = getAuth();
const db = getFirestore()
export {
GoogleAuthProvider,
auth,
signInWithPopup,
signOut,
signInAnonymously,
db,
doc,
onSnapshot,
getDoc
};
if (window.location.hostname === "localhost") {
connectAuthEmulator(auth, "http://localhost:9099");
connectFirestoreEmulator(db, "https://localhost", 8080)
}
This is for a username form, here is the react component that handles the form:
function Username() {
const [formValue, setFormValue] = useState("");
const [isValid, setIsValid] = useState(false);
const [loading, setLoading] = useState(false);
const { user, username } = useContext(UserContext);
useEffect(() => {
checkUsername(formValue);
}, [formValue]);
const onChange = (e) => {
const val = e.target.value.toLowerCase();
const regex = /^(?=[a-zA-Z0-9._]{3,15}$)(?!.*[_.]{2})[^_.].*[^_.]$/;
if (val.length < 3) {
setFormValue(val);
setLoading(false);
setIsValid(false);
}
if (regex.test(val)) {
setFormValue(val);
setLoading(true);
setIsValid(false);
}
};
const checkUsername = useCallback(
debounce(async (username: string) => {
if (username.length >= 3) {
const ref = doc(db, `usernames/${username}`);
const docSnap = await getDoc(ref);
if (docSnap.exists()) {
setIsValid(true);
setLoading(false);
} else {
setIsValid(false);
setLoading(false);
}
}
}, 500),
[]
);
return (
!username && (
<section>
<h3>Choose Your Username</h3>
<form>
<input
name="username"
placeholder="username"
value={formValue}
onChange={onChange}
/>
<button
type="submit"
className="btn-green"
disabled={!isValid}
>
Take Username
</button>
<h3>Debug State</h3>
<div>
Username: {formValue}
<br />
Loading: {loading.toString()}
<br />
Username Valid: {isValid.toString()}
</div>
</form>
</section>
)
);
}
It has some regex checks and length checks but if it passes those goes to firestore, when I enter in a username that is already taken (if it has been taken there would be a firestore document that exist with the username as the name of the doc) it takes a few seconds to show an error, but when i put in a valid username that has not been taken it gives my a error instantly. Can you please help me?
Thanks
The Failed to get document because the client is offline
error occurs when you attempt to retrieve a single document, you are offline, and the requested document is absent from the local cache. In this case, the client has no information about the document and does not know if it exists on the backend. There is no way to know if the document exists until a network connection is re-established. So to avoid incorrectly reporting that a document does not exist, this error is produced.
This situation can largely be avoided by getting the document while in an online state so that it is present in the local cache. Note, however, that the local cache is a cache, and "stale" entries may be evicted from the cache to make room for other data. If the data in the document does not change frequently, you could also instead register a snapshot listener via addSnapshotListener() and keep the listener alive to ensure that the document remains in the local cache.
Also, there can be multiple other reasons for this error, some them are :
You tried to reference the collection/ document which was not yet created. So, just create a collection with a dummy doc or just wait for the document to be created and see if it works fine.
Implement try ... catch around all get().await() calls. Unhandled exceptions can lead to this error
Issue was fixed by changing the port number of the emulator. Try for 8081 if 8080 is not working,
Also as per the Firebase documentation and GitHub link change,
if (window.location.hostname === "localhost") {
connectAuthEmulator(auth, "http://localhost:9099");
connectFirestoreEmulator(db, "https://localhost", 8080) }
to
if (window.location.hostname === "localhost") {
connectAuthEmulator(auth, "http://localhost:9099");
connectFirestoreEmulator(db, "localhost", 8080) }
Update all packages and enable offline persistence for your web app like this :
firebase.firestore().enablePersistence()
https://firebase.google.com/docs/firestore/manage-data/enable-offline
[5] This may also happen because OnCompleteListener is triggered when the task is completed, either it fails or succeeds. To avoid the error, you can use separate OnSuccessListener and OnFailureListener
OnSuccessListener is called when the task is succeeded, but if the above error occurs, OnFailureListener will be triggered and you can handle the error in onFailure() method of the listener.
[6] Check for correct Project ID when passing Project ID to initializeApp or setting environment variable.
admin.initializeApp({ projectId: "your-project-id" });
Or
export GCLOUD_PROJECT="your-project-id"
If still facing issues, try following this documentation to connect your app to Firestore Emulator and I'd also recommend enabling debug logging to see if it gives a hint as to why the client can't connect.
Thanks to Gourav B I was able to find the answer. I just had to remove the https:// in front of localhost in the connectFirestoreEmulator. I guess in the connectFirestoreEmulator they connect to https:// + host + port (the host and port are the second and third parameter). Thanks Gourav B!

React – Async / Await doesn’t seem to be waiting

I had previously posted this problem under the error message of “Cannot read property 'data' of undefined”. But in the process of digging into it for several hours, I’ve discovered that my problem really boils down to the fact that my “async / await” doesn’t seem to be . . . . awaiting! And yes, I did check the several other versions of this question that have already been asked here. :)
I’m building a React form that will have several drop-down boxes populated with data from MongoDB. I’m relatively new to React and a beginner with MongoDB.
I’ve been able to successfully get the data into the drop-down by just cramming all of the code into one file. Now, I’m trying to start refactoring the code by properly splitting pieces into separate files. And that’s where I’ve run into this “data delay” issue.
When “componentDidMount” calls the “fetchProjects” function, that function goes and grabs a list of projects from MongoDB using the “getProjects” function in a “projects service” file. When the console.log within “fetchProjects” runs, it’s coming back as undefined. But then after the data set comes back as undefined (and errors out the process), I do get a console log of the projects object array from the “getProjects” function.
I’ve been able to make this process work with hard-coded object array data in the “getProjects” function, so that tells me that the problem lies in the amount of time required to actually get the data back from MongoDB.
Please tell me there’s a way to solve this without using Redux! :D
Here’s my App.js file –
import React, { Component } from "react";
import "./App.css";
import { getProjects } from "./services/svcProjects";
class App extends Component {
state = {
data: {
selProject: ""
},
projects: []
};
async componentDidMount() {
await this.fetchProjects();
}
async fetchProjects() {
const { data: projects } = await getProjects();
console.log(projects);
this.setState({ projects });
}
render() {
return (
<div className="App">
<h1>Project Log</h1>
<label htmlFor={"selProject"}>{"Project"}</label>
<select name={"selProject"} id={"selProject"} className="form-control">
<option value="" />
{this.state.projects.map(a => (
<option key={a._id} value={a.project}>
{a.project}
</option>
))}
</select>
</div>
);
}
}
export default App;
And here’s the “projects service” file. Again, please note that the console.log statements here show that I’m still getting data back from MongoDB. That data is just taking too long to arrive back in the App.js file.
Also, by the way, I realize that having my Mongo connection info in this file is a huge security hole. I’ll be fixing that later.
import {
Stitch,
RemoteMongoClient,
AnonymousCredential
} from "mongodb-stitch-browser-sdk";
export function getProjects() {
const client = Stitch.initializeDefaultAppClient("------");
const db = client
.getServiceClient(RemoteMongoClient.factory, "-----")
.db("----------");
client.auth
.loginWithCredential(new AnonymousCredential())
.then(() =>
db
.collection("--------")
.find({}, { sort: { Project: 1 } })
.asArray()
)
.then(res => {
console.log("Found docs", res);
console.log("[MongoDB Stitch] Connected to Stitch");
return res;
})
.catch(err => {
console.error(err);
});
}
I think adding a return into your getProjects() service will solve your issue.
import {
Stitch,
RemoteMongoClient,
AnonymousCredential
} from "mongodb-stitch-browser-sdk";
export function getProjects() { //add async if you need to await in func body
const client = Stitch.initializeDefaultAppClient("------");
const db = client
.getServiceClient(RemoteMongoClient.factory, "-----")
.db("----------"); // if these above are async, then await them as well.
// added return keyword here
return client.auth // should return Promise to await in your component
.loginWithCredential(new AnonymousCredential())
.then(() =>
db
.collection("--------")
.find({}, { sort: { Project: 1 } })
.asArray()
)
.then(res => {
console.log("Found docs", res);
console.log("[MongoDB Stitch] Connected to Stitch");
return res;
})
.catch(err => {
console.error(err);
});
}
Edit 1:
For refactoring, I think pairing redux and redux-saga will give you very good separation of concern and a way to easily write test if you plan to do so.
But overall, I think this tweak above can at least solve your issue.

Resources