I encountered a problem in my chat app.
I works when I post message doc to the messages col but then I'm trying do getDocs back and render them I get an empty array.
I looked through FB docs, and I didn't notice any mistakes on my part. I also read an article where I was advised to use the react-firebase library with useCollectionData with which I had the same result.
const [messages, loading] = useCollectionData(
firestore.collection('messages').orderBy('createdAt')
)
I tried different approaches but nothing seems to work.
import React, { useState, useEffect } from 'react'
import { auth, db, app } from '../../firebase.config'
import { useAuthState } from 'react-firebase-hooks/auth'
import { useCollectionData } from 'react-firebase-hooks/firestore'
import { docs, onSnapshot, query, where, addDoc, collection, serverTimestamp, orderBy, getDocs } from 'firebase/firestore'
import Message from '../Message/Message'
import Spinner from '../Spinner/Spinner'
import './chat.css'
const Chat = () => {
const [user, loading, error] = useAuthState(auth)
const [value, setValue] = useState('')
const [msgs, setMsgs] = useState([])
console.log('msgs>>>>', msgs)
useEffect(() => {
const fetchMsg = async () => {
const messagesRef = collection(db, 'messages')
const q = query(
messagesRef,
orderBy('timestamp', 'desc')
)
const querySnap = await getDocs(q)
let listings = []
querySnap.forEach((doc) => {
return listings.push({
id: doc.id,
data: doc.data(),
})
})
setMsgs(listings)
}
fetchMsg()
}, [])
const sendMessage = async (e) => {
e.preventDefault();
const docRef = await addDoc(collection(db, 'messages'), {
uid: user.uid,
displayName: user.displayName,
photoURL: user.photoURL,
text: value,
createdAt: serverTimestamp()
})
console.log(docRef)
setValue('')
}
if (loading) {
return <Spinner />
}
return (
<>
<div className='ch-wind'>
{msgs.map((msg) => (
<Message key={msg.id} msg={msg} style={{ backgroundColor: user.uid === msg.uid ? '#A32cc4' : '#a1045a' }} />
))}
</div>
<form className="ch-form" onSubmit={sendMessage}>
<textarea
value={value}
className='ch-form-text'
onChange={e => setValue(e.target.value)}
placeholder='Enter your message here'
/>
<button
className='ch-form-btn'
>
Send
</button>
</form>
</>
)
}
export default Chat
By using useEffect() hook, I would assume that you want to get the data realtime. Firestore has a realtime listeners that you can use. You can listen to a document with the onSnapshot() method. An initial call using the callback you provide creates a document snapshot immediately with the current contents of the single document. Then, each time the contents change, another call updates the document snapshot. See code below:
useEffect(() => {
const messagesRef = query(collection(db, 'messages'), orderBy('timestamp', 'desc'));
onSnapshot(messagesRef, (snapshot) => {
// Maps the documents and sets them to the `msgs` state.
setMsgs(snapshot.docs.map(doc => ({
id: doc.id,
data: doc.data()
})))
})
}, [])
Also, as pointed out by #CDoe, you should use the same Fieldname which you set from the addDoc method as you can see on the above code.
Then on the rendering, something like this:
{msgs.map((msg) => (
// By setting the `doc.data()` to the object `data`, you should access it by `msg.data.<object_key>`
<Message key={msg.id} msg={msg.data.text} style={{ backgroundColor: user.uid === msg.data.uid ? '#A32cc4' : '#a1045a' }} />
))}
I leave some comments on the code to better understand it.
For more information on realtime updates, you may check out this documentation.
In the query, you're trying to orderBy timestamp. That's not a field you're creating in sendMessage.
When a value you're ordering by doesn't exist on the document, it won't return.
Maybe you meant to orderyBy the createdAt value.
const q = query(
messagesRef,
orderBy('createdAt', 'desc')
)
Related
I want to only show the "Load More" button when I have extra documents to show on my React and Firebase website.
Right now, I'm fetching only 2 documents from a firestore and I want to show the "Load More" button when I have more than 2 documents in my firestore. If I only have 2 or fewer than 2 documents in my firestore, I don't want to show the "Load More" button.
And I want to hide the "Load More" button after fetching all the documents that I have on the firestore.
Anyone, please help me with this!
useCollection Hook:
import { useEffect, useRef, useState } from "react"
// firebase import
import {
collection,
getDocs,
limit,
onSnapshot,
orderBy,
query,
startAfter,
where,
} from "firebase/firestore"
import { db } from "../firebase/config"
export const useCollection = (c, _q, _l, _o) => {
const [documents, setDocuments] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
// if we don't use a ref --> infinite loop in useEffect
// _query is an array and is "different" on every function call
const q = useRef(_q).current
const o = useRef(_o).current
useEffect(() => {
let ref = collection(db, c)
if (q) {
ref = query(ref, where(...q))
}
if (o) {
ref = query(ref, orderBy(...o))
}
if (_l) {
ref = query(ref, limit(_l))
}
const unsubscribe = onSnapshot(ref, (snapshot) => {
const results = []
snapshot.docs.forEach(
(doc) => {
results.push({ ...doc.data(), id: doc.id })
},
(error) => {
console.log(error)
setError("could not fetch the data")
}
)
// update state
setDocuments(results)
setIsLoading(false)
setError(null)
})
// unsubscribe on unmount
return unsubscribe
}, [])
const fetchMore = async (doc) => {
setIsLoading(true)
const q = query(
collection(db, c),
orderBy(...o),
startAfter(doc.createdAt),
limit(_l)
)
const newDocuments = await getDocs(q)
updateState(newDocuments)
setIsLoading(false)
}
const updateState = (docs) => {
if (!docs.empty) {
const tempPosts = []
docs.forEach((document) => {
tempPosts.push({
id: document.id,
...document.data(),
})
})
setDocuments([...documents, ...tempPosts])
}
}
return { documents, fetchMore, error, isLoading }
}
SolutionComments.js (In this file I'm showing the "Load More Comments" button)
import React, { useState } from "react"
import { useParams } from "react-router-dom"
import { useCollection } from "../../hooks/useCollection"
import Comment from "./Comment"
import CommentForm from "./CommentForm"
const SolutionComments = () => {
const [activeComment, setActiveComment] = useState(null)
const { id } = useParams()
const { documents, fetchMore, isLoading } = useCollection(
`solutions/${id}/comments`,
null,
2,
["createdAt", "desc"]
)
const fetchMoreComments = () => {
fetchMore(documents[documents.length - 1])
}
return (
<div className="mt-10">
<CommentForm docID={id} />
<div>
{documents &&
documents.map((comment) => (
<Comment
key={comment.id}
comment={comment}
replies={comment.replies}
activeComment={activeComment}
setActiveComment={setActiveComment}
/>
))}
</div>
{documents.length > 2 && (
<button onClick={fetchMoreComments} className="text-white bg-purple-500">
{!isLoading ? "Load More Comments!" : "Loading..."}
</button>
)}
</div>
)
}
export default SolutionComments
Firestore does not have a mechanism for automatically telling you how many documents are in a query. You will need to manage that aggregate count yourself. Then you could fetch that aggregate count and use that to determine whether to show a load more button or not.
However this is harder than it sounds. And most modern apps we don't use load more buttons rather we use infinite scroll.
To be honest, it has never been efficient or smart to show a result set count in an app. The fact that people have done it in the past doesn't mean that it is the right thing to do today. It might have made sense when you had small databases, typically running off of a desktop database. But in a cloud-based solution with millions of documents and millions of users, and complex queries, knowing how many documents are in a result set is a very hard problem to solve.
I want to update a 'progress' field in the document with autogenerated id (using fire store)every time the progress button is clicked.
I tried in several different ways but nothing seems to be working. I am attaching my method:
useEffect(() => {
var user = firebase.auth().currentUser;
var email = user.email;
var documentID = database.collection("ChosenChallenge").doc().id;
database
.collection("Users")
.doc(email)
.collection("ChosenChallenge")
.doc(documentID)
.update({
Progress: percentRange
})
.then(() => {
console.log('added!');
});
}, )
and the class:
import React, { useState, useEffect } from "react";
import './newprogressbar.css';
import { firebaseAppAuth, database } from "../firebase";
import firebase from "firebase/app";
export const ProgressBarContainer = (props) => {
let [percentRange, setProgress] = useState(0);
const handleUpdate = () => {
setProgress(percentRange < 99 ? percentRange + 7.14285714 : 100);
props.onChange(percentRange + 7.14285714);
}
const Range = (props) => {
return (
<div className="range" style={{width: `${props.percentRange}%`}}/>
);
};
const ProgressBar = (props) => {
return (
<div className="progress-bar">
<Range percentRange={props.percentRange}/>
</div>
);
};
useEffect(() => {
var user = firebase.auth().currentUser;
var email = user.email;
var documentID = database.collection("ChosenChallenge").doc().id;
database
.collection("Users")
.doc(email)
.collection("ChosenChallenge")
.doc(documentID)
.update({
Progress: percentRange
})
.then(() => {
console.log('added!');
});
}, )
return (
<div id="progresscontainer">
<ProgressBar percentRange={percentRange} />
<button id="updatepic" onClick={handleUpdate}>Daily Update</button>
</div>
);
};
Any help high appreciated!
database.collection("ChosenChallenge").doc().id generates a new random document ID every time it's called. It doesn't know which existing document you want.
If you want to update a document, you must know its existing ID. There is no avoiding that. If you don't know the ID, you will need to query the collection to find the document ID based on something you know about the contents of the document.
If you don't know the ID and you don't know anything about the contents of the document to filter a query, you're stuck, and you might need to rethink how you are using Firestore for this app.
This is my firestore database:
After useEffect() updated the workouts const, I have access to name and uId only. How can I have access to the document id as well?
import React, { useState, useEffect } from "react";
import "./Workouts.sass";
import Workout from "./Workout";
import firebase from "./firebase";
function Workouts() {
const [workouts, setWorkouts] = useState([]);
useEffect(() => {
let user = firebase.auth().currentUser;
const database = firebase.firestore();
const unsubscribe = database
.collection("workouts")
.where("uId", "==", user.uid)
.onSnapshot((snapshot) => {
setWorkouts(snapshot.docs.map((doc) => doc.data()));
});
return () => {
unsubscribe();
};
}, []);
return (
<div className="Workouts">
{workouts.map((workout) => (
<Workout key={workout.name} workout={workout} />
))}
</div>
);
}
export default Workouts;
You can use the snapshot doc.id
I like to add id as a property to the object:
snapshot.docs.map((doc) => ({id: doc.id, ...doc.data()}))
So this is going to be a bit long, but i will try to produce my issue quite simply. The full code of my component is at the end btw.
I am learning to use firebase authentication and adding and retrival of data from the firebase Firestone in react. I have the authentication set up and this gives me a unique ID that I can use to fetch the content I have added in real-time. The function that listen and fetches the data in the code below is listenForMessages() that is inside my useEffect hook. This function fetches the data updates the state in the content hook which then displays the data in my render. The issue is that right now to fetch the data I have to insert the unique ID (jYAhV0xCtmOEJtOPXHirYXkQtju1) directly into doc() inside this listenForMessages(). This is not ideal as I cannot be hardcoding the unique ID. So I switched things a little bit and passed the unique id I got from my app.js as a prop into this component. Thanks to the below bit of code the unique ID is now passed into the below usestate hook, which should then give me the freedom to use id in doc() in this manner= doc(id).
const [ id, setId ] = useState(props);
useEffect(
() => {
setId(props.uid);
},
[ props ]
);
The issue is that it simply doesn't work :/
When i add id into doc() I am met with this messege- FirebaseError: Function CollectionReference.doc() requires its first argument to be of type non-empty string, but it was: a custom Object object
can anyone help? :/
const [ id, setId ] = useState(props);
useEffect(
() => {
setId(props.uid);
},
[ props ]
);
import React, { useState, useEffect } from 'react';
import fire, { db } from '../Config/firebase';
export default function Home(props) {
function logout() {
fire.auth().signOut();
}
const [ name, setName ] = useState('');
const [ email, setEmail ] = useState('');
const [ Content, setContent ] = useState([]);
const [ id, setId ] = useState(props);
useEffect(
() => {
setId(props.uid);
},
[ props ]
);
const sendData = () => {
db
.collection('users')
.doc(props.uid)
.set({
name,
email,
id
})
.then(function() {
console.log('Document successfully written!');
})
.catch(function(error) {
console.error('Error writing document: ', error);
});
};
const listenForMessages = () => {
db.collection('users').doc('jYAhV0xCtmOEJtOPXHirYXkQtju1').onSnapshot(function(doc) {
const allMessages = [];
allMessages.push(doc.data());
setContent(allMessages);
});
};
useEffect(() => {
listenForMessages();
}, []);
return (
<div>
<h1>I am home</h1>
<button onClick={logout}>Log out</button>
<h1>Enter information</h1>
<form>
<label>Name</label>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
<label>Email</label>
<input type="text" value={email} onChange={(e) => setEmail(e.target.value)} />
</form>
<button onClick={sendData}>Send data</button>
{Content.map((n) => (
<p key={Math.floor(Math.random() * 10)}>
{n.name} {n.email}
</p>
))}
</div>
);
}
I'm working with Firebase - Cloud Firestore and at the moment I would like to paginate all the records available. I already have a list of records and what is left is some pagination for this. I'm new with Cloud Firestore, so any clarity is appreciated.
I checked the Firestore documentation (https://firebase.google.com/docs/firestore/query-data/query-cursors#paginate_a_query) and examples with ReactJS, but there is not much available.
I understand that eg:.startAt(0), .limit(10), but the question is how to paginate properly with this component called at the render method.
import React, { Component } from 'react';
import Pagination from "react-js-pagination";
import firestore from "./Firebase";
export default class DataList extends Component {
constructor(props) {
super(props);
this.state = {
dbItems: [],
currentPage: 1,
itemsPerPage: 3,
totalItemCount: 1,
activePage: 15
}
this.handlePageChange = this.handlePageChange.bind(this);
}
handlePageChange(pageNumber) {
console.log(`active page is ${pageNumber}`);
this.setState({ activePage: pageNumber });
}
async getItems() {
const { currentPage, itemsPerPage } = this.state;
const startAt = currentPage * itemsPerPage - itemsPerPage;
const usersQuery = firestore.collection('Users').orderBy("email").startAt(startAt).limit(itemsPerPage)
const snapshot = await usersQuery.get()
const items = snapshot.docs.map(doc => doc.data())
return this.setState({
dbItems: items,
totalItemCount: firestore.collection('Users').get().then(res => console.log(res.size))
})
}
componentDidMount() {
this.getItems()
}
componentDidUpdate(prevProps, prevState) {
const isDifferentPage = this.state.currentPage !== prevState.currentPage
if (isDifferentPage) this.getItems()
}
render() {
return (
<div>
{this.state.dbItems.map((users, index) => {
return (
<p key={index}>
<b>First Name:</b> {users.firstname} <br />
<b>Email:</b> {users.email}
</p>
)
})
}
<Pagination
activePage={this.state.activePage}
itemsCountPerPage={this.state.itemsPerPage}
totalItemsCount={this.state.totalItemCount}
pageRangeDisplayed={this.state.itemsPerPage}
onChange={this.handlePageChange}
/>
</div>
)
}
}
Thank you for the help!
Pagination can be achieved using startAt()
// Get Items.
async fetchUsers = () => {
// State.
const {users, usersPerPage} = this.state
// Last Visible.
const lastVisible = users && users.docs[users.docs.length - 1]
// Query.
const query = firestore.collection('Users')
.orderBy('email')
.startAfter(lastVisible)
.limit(usersPerPage)
// Users.
const users = await query.get()
// ..
return this.setState({users})
}
// Did Mount.
componentDidMount() {
this.fetchUsers()
}
// Did Update.
componentDidUpdate(prevProps, prevState) {
const isDifferentPage = this.state.currentPage !== prevState.currentPage
if (isDifferentPage) this.fetchUsers()
}
Anyone new to Firestore and Firestore Pagination with ReactJS that would be kinda confusing to understand how Pagination will work or when to trigger call to next set of documents in firestore. anyone struggle like this try my example to make some ideas and process ahead.(Im using React-Bootstrap to render UI Elements)
01 - Install Package react-infinite-scroll-component
First Install this package yarn add react-infinite-scroll-component
02 - Include Package
Include it to your file by 'import InfiniteScroll from 'react-infinite-scroll-component';' importing it
03 - Init State
initiate state with empty list array
this.state = {
list: [],
};
04 - Create Function to get first set of data and initiate it with component did mount
//component did mount will fetch first data from firestore
componentDidMount(){
this.getUsers()
}
getUsers(){
let set = this
//initiate first set
var first = set.ref.collection("users").limit(12);
first.get().then(function (documentSnapshots) {
// Get the last visible document
var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1];
//initiate local list
const list = [];
documentSnapshots.forEach(function(doc) {
//im fetching only name and avatar url you can get any data
//from your firestore as you like
const { name, avatar_full_url } = doc.data();
//pushing it to local array
list.push({ key: doc.id, name, avatar_full_url });
});
//set state with updated array of data
//also save last fetched data in state
set.setState({ list, last: lastVisible });
});
}
05 - Create function to get balance data set
fetchMoreData = () => {
let set = this
//get last state we added from getUsers()
let last = this.state.last
var next = set.ref.collection("users").startAfter(last).limit(12);
next.get().then(function (documentSnapshots) {
// Get the last visible document
var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1];
const list = [];
documentSnapshots.forEach(function(doc) {
//im fetching only name and avatar url you can get any data
//from your firestore as you like
const { name, avatar_full_url } = doc.data();
list.push({ key: doc.id, name, avatar_full_url });
});
//set state with updated array of data
//also save last fetched data in state
let updated_list = set.state.list.concat(list);
set.setState({ list: updated_list, last: lastVisible });
});
};
06 - Render UI
<InfiniteScroll
dataLength={this.state.list.length}
next={this.fetchMoreData}
hasMore={true}
loader={<span className="text-secondary">loading</span>}>
<Row className="mt-3">
{ this.state.list.map((single, index) => (
<Col lg={4} key={ index }>
<div>
<Image src={ single.avatar_full_url }roundedCircle width="100" />
<h2>{ single.name }</h2>
</div>
</Col>
))}
</Row>
</InfiniteScroll>
Check this example this could help anyone who trying previous / next pagination
//initial state
const [list, setList] = useState([]);
const [page, setPage] = useState(1);
//loading initial data
useEffect(() => {
const fetchData = async () => {
await firebase.firestore().collection('users')
.orderBy('created', 'desc') //order using firestore timestamp
.limit(5) //change limit value as your need
.onSnapshot(function(querySnapshot) {
var items = [];
querySnapshot.forEach(function(doc) {
items.push({ key: doc.id, ...doc.data() });
});
setList(items);
})
};
fetchData();
}, []);
After loading initial data use following function for next button trigger
//next button function
const showNext = ({ item }) => {
if(list.length === 0) {
//use this to show hide buttons if there is no records
} else {
const fetchNextData = async () => {
await firebase.firestore().collection('users')
.orderBy('created', 'desc') //order using firestore timestamp
.limit(5) //change limit value as your need
.startAfter(item.created) //we pass props item's first created timestamp to do start after you can change as per your wish
.onSnapshot(function(querySnapshot) {
const items = [];
querySnapshot.forEach(function(doc) {
items.push({ key: doc.id, ...doc.data() });
});
setList(items);
setPage(page + 1) //in case you like to show current page number you can use this
})
};
fetchNextData();
}
};
Then Previous button function
//previous button function
const showPrevious = ({item}) => {
const fetchPreviousData = async () => {
await firebase.firestore().collection('users')
.orderBy('created', 'desc')
.endBefore(item.created) //this is important when we go back
.limitToLast(5) //this is important when we go back
.onSnapshot(function(querySnapshot) {
const items = [];
querySnapshot.forEach(function(doc) {
items.push({ key: doc.id, ...doc.data() });
});
setList(items);
setPage(page - 1)
})
};
fetchPreviousData();
};
at the end create list view & two buttons like this
{
//list doc's here this will come inside return (place this code inside table)
list.map((doc) => (
<tr key={doc.key}>
<td>{ doc.name }</td>
<td>{ doc.age }</td>
<td>{ doc.note }</td>
</tr>
))
}
{
//show previous button only when we have items
//pass first item to showPrevious function
page === 1 ? '' :
<Button onClick={() => showPrevious({ item: list[0] }) }>Previous</Button>
}
{
//show next button only when we have items
//pass last item to showNext function
list.length < 5 ? '' :
<Button onClick={() => showNext({ item: list[list.length - 1] })}>Next</Button>
}
That's it check my code comments where you can change as per your need. this is what happens when you paginate using Firebase FireStore. you can use create custom hook to reuse these component as per your need.
Hope this could help someone so i made a gist check it here
here AddTable and AddForm is adding table and add form to fill data in table...
import React, { useEffect, useState } from "react";
import Button from "react-bootstrap/Button";
import Pagination from "react-bootstrap/Pagination";
import AddTable from "../management/AddTable";
import AddForm from "../management/AddSuperAdminForm";
import {
where,
getDocs,
collection,
query,
orderBy,
startAfter,
limit,
endBefore,
limitToLast,
} from "firebase/firestore";
import { db_firestore } from "../../../firebase.config";
const SuperAdmin = () => {
const [tableDataArray, setTableDataArray] = useState();
const [show, setShow] = useState(false);
const [editId, setEditId] = useState("");
const [oldUid, setOldUid] = useState("");
const [lastVisible, setLastVisible] = useState();
const [prevVisible, setPrevVisible] = useState();
const handleClose = () => {
setShow(false);
setEditId("");
};
const handleShow = () => {
setShow(true);
setEditId("");
};
let tempdata;
let pageSize = 3;
let q = query(
collection(db_firestore, "users"),
where("role", "==", "superadmin"),
orderBy("timestamps", "desc"),
limit(pageSize)
);
function nextPage(lastVisible) {
q = query(
collection(db_firestore, "users"),
where("role", "==", "superadmin"),
orderBy("timestamps", "desc"),
startAfter(lastVisible),
limit(pageSize)
);
}
function prevPage(firstVisible) {
q = query(
collection(db_firestore, "users"),
where("role", "==", "superadmin"),
orderBy("timestamps", "desc"),
endBefore(firstVisible),
limitToLast(pageSize + 1)
);
}
const newfun = async () => {
const querySnapshot = await getDocs(q);
tempdata = [];
// Get the last visible document
setLastVisible(querySnapshot.docs[querySnapshot.docs.length - 1]);
// Get the prev visible document
setPrevVisible(querySnapshot.docs[0]);
querySnapshot.forEach((doc) => {
const { name, email, uid } = doc.data();
tempdata.push([name, email, uid, doc.id]);
});
console.log("SuperAdmin...");
setTableDataArray(tempdata);
};
useEffect(() => {
newfun();
// setInterval(() => { // if you want to get new update after some secound
// newfun();
// }, 10000);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div>
<Button
className="d-block mx-auto my-2"
variant="primary"
onClick={handleShow}
>
Add SuperAdmin
</Button>
{/* -----> AddTable <------
Index will generate Automatic In Table.
Always keep action end of the table.
*/}
{tableDataArray ? (
<AddTable
tableHeaders={["Name", "Email", "uid", "Action"]}
tableData={tableDataArray}
fetchNew={newfun}
setEditId={setEditId}
setShow={setShow}
setOldUid={setOldUid}
/>
) : (
""
)}
<AddForm
fetchNew={newfun}
show={show}
setShow={setShow}
handleClose={handleClose}
editId={editId}
oldUid={oldUid}
/>
<Pagination className="float-end">
<Pagination.Item
className="shadow-none"
size="lg"
onClick={() => {
prevPage(prevVisible);
newfun();
}}
>
Previous
</Pagination.Item>
<Pagination.Item
className="shadow-none"
size="lg"
onClick={() => {
nextPage(lastVisible);
newfun();
}}
>
Next
</Pagination.Item>
</Pagination>
</div>
);
};
export default SuperAdmin;
Use startAt() or startAfter() for that
firestore
.collection("Users")
.startAt(0)
.limit(10)
.get()