I am creating a data context for my react native app. There is an array of property objects in a firebase node and the aim is to pull this data into the app on load and provide that data throughout the app. I have a loader that shows if the loading state is true and should only be false if the data has been successfully pulled from firebase.
The issue I am having is the Loader ends before all the data is called and then the properties are not shown on the home page of the app until i refresh the app again.
Below is the code for the app:
import { onValue, ref } from 'firebase/database'
import React, { useEffect, useState } from 'react'
import { projectDatabase } from '../../config'
import useAuth from '../hooks/useAuth'
export const DataContext = React.createContext()
const DataLayer = ({ children }) => {
const { userCredentials: user } = useAuth() //get user data
const [company, setCompany] = useState('');
const [loading, setLoading] = useState(true);
const [properties, setProperties] = useState([]);
useEffect(() => {
const userRef = ref(projectDatabase, 'users/' + user.uid + '/company');
onValue(userRef, (snapshot) => {
setCompany(snapshot.val());
const propertiesRef = ref(projectDatabase, 'providers/' + snapshot.val() + '/properties');
onValue(propertiesRef, (propertiesSnapshot) => {
// console.log(propertiesSnapshot.val())
setProperties(propertiesSnapshot.val());
setLoading(false);
});
});
}, [])
return (
<DataContext.Provider
value={{
properties:properties,
company: company,
loading: loading,
userEnquiries:userEnquiries,
chatId:chatId,
acceptedBookings:acceptedBookings
}}
>
{children}
</DataContext.Provider>
)
}
export default DataLayer
Is there a way to make the call to the firebase RTDB complete BEFORE the loading is resolved as false or do i just do a useEffect call to every page the data is needed? For context the array has 200 properties.
I would really appreciate any help I can get. I love firebase and would really want this to work.
Related
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])
I am currently working on developing a Whatsapp Web clone exactly similar to 'Whatsapp Web' using Whatsapp Web JS and Next.JS and Express, the data is coming from real whatsapp with the help of Wwebjs module (link above^)
This is the folder structure
|
|
|-context
|--> (StateContext.js) Contains the data that has been fetched using useEffect() and then further sent down the tree using useContext()
|-pages
|--> Components (Contains all the components)
|--> (_app.js) (Contains the <State.Provider>)
|--> (index.js)
StateContext.js
import React, { createContext, useContext, useState, useEffect } from "react";
import axios from "axios";
import { io } from "socket.io-client";
import useSound from 'use-sound';
const Context = createContext();
const socket = io('http://localhost:8000')
export const StateContext = ({ children }) => {
//Contains msgs from all the chats in ("phone_no.": "messages_from_this_no.") <- this pattern as objects
const [allMsgs, setAllMsgs] = useState();
//Contains info of the current logged in user
const [currentUser, setCurrentUser] = useState();
//Contains the chats for all phone no`s.
const [chats, setChats] = useState([]);
//Contains the latest msgs received from chats
const [lastMsgs, setLastMsgs] = useState([]);
//Chat that has been clicked on to see messages from that chat
const [selectedChat, setSelectedChat] = useState();
//Loading state while messages and other data is being fetched
const [loading, setLoading] = useState(false);
//Notification sound
// const [play] = useSound('/notification.mp3');
useEffect(() => {
socket.on('connect', () => {
console.log(socket.id);
})
socket.on('disconnect', () => {
console.log(socket.id + 'disconnected.');
})
//Triggers when a message has been received
socket.on('newMessage', (msg) => {
updateChats(msg, lastMsgs, chats)
})
return async () => {
socket.off('connect');
socket.off('disconnect');
socket.off('newMessage');
setLoading(true)
const chats_res = await axios.get('http://localhost:8000/chats');
const user_res = await axios.get('http://localhost:8000/users/me');
const allMessages_res = await axios.get('http://localhost:8000/messages');
const lastMsgs_res = await axios.get('http://localhost:8000/messages/last');
setChats(chats_res.data.response);
setCurrentUser(user_res.data);
setLastMsgs(lastMsgs_res.data.response);
setAllMsgs(allMessages_res.data.response);
setLoading(false)
}
}, []);
// function used to update data when a new message has been received to update the last message and the message unread count
function updateChats(newMessage, lastMessages, chats) {
console.log('Updating Chats');
const updatedLastMsgs = lastMessages.map((lastMsg) => {
if (newMessage.from === lastMsg?.from) {
return newMessage
}
return lastMsg
});
const newChats = chats.map((chat) => {
if (chat.id._serialized === newMessage.from) {
chat.unreadCount += 1;
console.log(chat.unreadCount);
}
return chat
});
setLastMsgs(updatedLastMsgs);
setChats(newChats)
console.log(newChats);
}
return <Context.Provider value={{ allMsgs, lastMsgs, currentUser, chats, loading, selectedChat, msgs, setAllMsgs, setChats, setCurrentUser, setLastMsgs, setSelectedChat, setMsgs }}>
{children}
</Context.Provider>
}
export const useStateContext = () => useContext(Context)
Now after the data has been fetched and later on a message is received, the updateChats() function is executed but after execution the chats vanishes. On console logging the "const newChats" it logs undefined, but for some reason when the next app reloads due to server side changes on refresh it seems to work magically and absolutely fine.
Well I think it is due to the socket.on('newMessage') receiving the state data when it is yet to be fetched and after that refresh it gets the data so it start working.
So what is it that I could do to get this working right?
I am trying to implment a Chat functionality in my React Native App.
The Problem i am facing is, that if i send a message it doesnt get saved in the Firestore Database. But if i remove the useLayoutEffect Function, the Messages get Saved in Firestore.
Ive got the code from this Page:
Here is my Code:
import React, {useCallback, useLayoutEffect, useState} from 'react';
import {addDoc, collection, query, orderBy, onSnapshot} from 'firebase/firestore';
import {db} from '../../../firebase';
import {GiftedChat} from 'react-native-gifted-chat';
export const EventChatScreen = () => {
const [messages, setMessages] = useState([])
useLayoutEffect(() => {
const collectionRef = collection(db, 'chats');
const q = query(collectionRef, orderBy('createdAt', 'desc'));
return onSnapshot(q, QuerySnapshot =>
{
setMessages(
QuerySnapshot.docs.map(doc => ({ // these key:value pairs are in a required format for GiftedChat
_id: doc.data()._id,
createdAt: doc.data().createdAt.toDate(),
text: doc.data().text,
user: doc.data().user
}))
);
});
}, []);
const onSend = useCallback((messages = []) => {
setMessages(previousMessages =>
GiftedChat.append(previousMessages, messages)
);
const { _id, createdAt, text, user } = messages[0];
addDoc(collection(db, 'chats'), {
_id,
createdAt,
text,
user
});
}, []);
return (
<GiftedChat
messages={messages}
showAvatarForEveryMessage={true}
onSend={messages => onSend(messages)}
user={{
_id: 1,
avatar: 'https://i.pravatar.cc/300'
}}
/>
);
}
export default EventChatScreen
As i mentioned before. As soon as i remove the useLayoutEffect it saves the Data to Firebase.
Ive also tried to get the Return Value of the Add Doc. But as soon as the useLayoutEffect function is added, it doesnt return a Value.
EDIT: Now it works while i am in the Debug Mode.
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 currently creating a React Native mobile application with Typescript.
The application uses the Firebase authentication with the Google OAuth Provider.
In order to use the username and some other details (retrieved from Firestore) I'm using a React Provider like shown in the following example:
import React, {useState, useEffect} from 'react';
import auth from '#react-native-firebase/auth';
import { GoogleSignin } from '#react-native-community/google-signin';
import firestore from '#react-native-firebase/firestore';
GoogleSignin.configure({
webClientId: 'x.googleusercontent.com',
});
const getUserById = async (id: string) => {
const admin = await firestore().collection("users").doc(id).collection("priv").doc("admin").get();
const prot = await firestore().collection("users").doc(id).collection("priv").doc("protected").get();
const jsonData = {
admin: admin.data(),
protected: prot.data(),
};
return jsonData;
}
const AuthContext = React.createContext({});
function AuthProvider(props: any) {
const [user, setUser] = useState(auth().currentUser);
const [details, setDetails] = useState({});
const [initializing, setInitializing] = useState(true);
const onAuthStateChanged = async (authUser: any) => {
setUser(authUser);
if (authUser !== null)
refreshDetails();
}
const refreshDetails = async () => {
const details = (await getUserById(user.uid));
setDetails(details);
}
useEffect(() => {
const subscriber = auth().onAuthStateChanged(onAuthStateChanged);
return subscriber; // unsubscribe on unmount
}, []);
const loginWithGoogle = async () => {
const { idToken } = await GoogleSignin.signIn();
// Create a Google credential with the token
const googleCredential = auth.GoogleAuthProvider.credential(idToken);
// Sign-in the user with the credential
return auth().signInWithCredential(googleCredential);
}
const logout = () => {
auth()
.signOut()
}
return (
<AuthContext.Provider value={{user, loginWithGoogle, logout, refreshDetails, details, initializing}} {...props}></AuthContext.Provider>
)
}
const useAuth = () => {
const state = React.useContext(AuthContext);
return {
...state,
};
}
export {AuthProvider, useAuth};
As you can see in the example I'm using this useEffect method from React to subscribe to authentication changes.
Unfortunately if I close the app and reopen it again, this authentication change isn't triggered so the user state isn't set and I get a bunch of errors.
What would be the best practice in a scenario like this? I think I only need to trigger the onAuthStateChangeEvent when the app was started again.
Thanks for all help
IJustDev
onAuthStateChanged function must be triggered when the app re-opens. However, it's supposed to run asynchronously you have to implement the case user's value is invalid.