react firebase firestore empty useEffect useState - javascript

having an issue, when the when nav to the comp the items state is empty, if I edit the code and page refreshes its shows up and if I add the state to the useEffect "[itemCollectionRef, items]" it's an inf loop but the data is their anyone have a better idea or way to fetch the data for display from firestore.
import React, { useState, useEffect } from "react";
import { Grid, Box, Button, Space } from "#mantine/core";
import { ItemBadge } from "../../components/NFAItemBadge";
import { useNavigate } from "react-router-dom";
import { db, auth } from "../../firebase";
import { getFirestore, query, getDocs, collection, where, addDoc } from "firebase/firestore";
import { useAuthState } from "react-firebase-hooks/auth";
const ItemTrack = () => {
const [user, loading, error] = useAuthState(auth);
const navigate = useNavigate();
const [items, setItems] = useState([]);
const itemCollectionRef = collection(db, "items");
useEffect(() => {
//if(!user) return navigate('/');
//if(loading) return;
const q = query(itemCollectionRef, where("uid", "==", user.uid));
const getItems = async () => {
const data = await getDocs(q);
setItems(data.docs.map((doc) => ({ ...doc.data(), id: doc.id })));
console.log("Fetched Items: ", items);
};
getItems();
}, []);
if (loading) {
return (
<div>
<p>Initialising User....</p>
</div>
);
}
if (error) {
return (
<div>
<p>Error: {error}</p>
</div>
);
}
if (user) {
return (
<Box sx={{ maxWidth: 1000 }} mx="auto">
</Box>
);
} else {
return navigate("/");
}
};
export default ItemTrack;

It will depend how you will render the data from the useEffect. setState does not make changes directly to the state object. It just creates queues for React core to update the state object of a React component. If you add the state to the useEffect, it compares the two objects, and since they have a different reference, it once again fetches the items and sets the new items object to the state. The state updates then triggers a re-render in the component. And on, and on, and on...
As I stated above, it will depend on how you want to show your data. If you just want to log your data into your console then you must use a temporary variable rather than using setState:
useEffect(() => {
const newItems = data.docs.map((doc) => ({ ...doc.data(), id: doc.id }))
console.log(newItems)
// setItems(newItems)
}, [])
You could also use multiple useEffect to get the updated state object:
useEffect(() => {
setItems(data.docs.map((doc) => ({ ...doc.data(), id: doc.id })))
}, [])
useEffect(() => { console.log(items) }, [items])
If you now want to render it to the component then you have to call the state in the component and map the data into it. Take a look at the sample code below:
useEffect(() => {
const q = query(itemCollectionRef, where("uid", "==", user.uid));
const getItems = async () => {
const data = await getDocs(q);
setItems(data.docs.map((doc) => ({ ...doc.data(), id: doc.id })));
};
getItems();
}, []);
return (
<div>
<p>SomeData: <p/>
{items.map((item) => (
<p key={item.id}>{item.fieldname}</p>
))}
</div>
);

Related

TypeError: Cannot read properties of null (reading 'useState') in Next.js

I'm getting this error message TypeError: Cannot read properties of null (reading 'useState') when I use my custom hooks inside the getStaticProps to fetch the data from the firebase firestore. Anyone, please help me with this?
Challenges page code:
import Card from "../components/reusable/Card"
import { useCollection } from "../hooks/useCollection"
const Challenges = ({ challenges }) => {
return (
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6 justify-items-center mt-8">
{challenges.map((challenge) => {
return (
<Card key={challenge.id} card={challenge} />
)
})}
</div>
)
}
export default Challenges
export async function getStaticProps() {
const { documents, isLoading } = useCollection("challenges", null, null, [
"createdAt",
"desc",
])
return {
props: {
challenges: documents,
},
}
}
useCollection hook code:
import { useEffect, useState } from "react"
import { collection, limit, onSnapshot, orderBy, query, 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)
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)
})
return () => unsubscribe()
}, [])
return { documents, error, isLoading }
}
You can use useState or any hook only inside a React Component, or a custom hook as Rules of Hooks says, and getStaticProps is none of the two. Plus, it only runs at build time, so not sent to the browser:
If you export a function called getStaticProps (Static Site Generation) from a page, Next.js will pre-render this page at build time using the props returned by getStaticProps.
You could either move the data fetching to the client and get rid of getStaticPros, like so:
import Card from "../components/reusable/Card";
import { useCollection } from "../hooks/useCollection";
const Challenges = () => {
const {
documents: challenges,
isLoading,
error,
} = useCollection("challenges", null, null, ["createdAt", "desc"]);
if (isLoading) {
return <p>Loading...</p>;
}
if (error) {
return <p>Error happend</p>;
}
return (
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6 justify-items-center mt-8">
{challenges.map((challenge) => {
return <Card key={challenge.id} card={challenge} />;
})}
</div>
);
};
export default Challenges;
Or transform useCollection to a normal async function without any hook in it, and use it inside getStaticProps. But it doesn't seem like it's what you want, since you are creating a subscription on the client and all.

Uncaught FirebaseError: Invalid query. You must not call startAt() or startAfter() before calling orderBy()

I'm trying to add functionality to fetch the next 4 comments but when I click on the Load more comments button, it throws this error message.
Uncaught FirebaseError: Invalid query. You must not call startAt() or startAfter() before calling orderBy().
Anyone, please help me with this.
SolutionComments.js
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 } = useCollection(`solutions/${id}/comments`, null, 4, [
"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>
<button onClick={fetchMoreComments} className="text-white bg-purple-500">
Load More Comments!
</button>
</div>
)
}
export default SolutionComments
useCollection hook:
import { useEffect, useRef, useState } from "react"
// firebase import
import {
collection,
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
let ref = collection(db, c)
useEffect(() => {
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
}, [c, q, _l, o, isLoading])
// I'm using this function to fetch next 4 comments including the previous ones.
const fetchMore = (doc) => {
ref = query(ref, orderBy(...o), startAfter(doc), limit(_l))
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
console.log(results)
setDocuments([...documents, results])
setIsLoading(false)
setError(null)
})
}
return { documents, fetchMore, error, isLoading }
}
You are using onSnapshot() to start your query. Since you are doing paging, it is likely you want to use getDocs() instead.
At the very least, you want to be sure to shut down your previous listener for onSnapshot() before starting a new one. But it is rarely all that useful/correct to use an onSnapshot() with paging. Things get hairy-complicated under that approach.

React useEffect updating with default initialized useState

I'm trying to create an array state variable called usersInfos that I can append to whenever I do an api call, but useEffect is updating with the initial value of the userData state variable.
// api.js
import axios from 'axios';
export function fetchUserData () {
return axios.get('https://randomuser.me/api')
.then(res => {
return res;
})
.catch(err => {
console.error(err);
})
}
import { fetchUserData } from '../../src/api';
import { useState, useEffect } from 'react';
import ProfileCard from './profilecard';
export default function UserProfile() {
const [userData, setUserData] = useState();
const [usersInfos, setUsersInfos] = useState([]);
const getUserData = async () => {
let ud = await fetchUserData()
setUserData(ud);
}
useEffect(() => {
getUserData();
}, [])
useEffect(() => {
const newInfos = [
...usersInfos,
userData,
]
setUsersInfos(newInfos);
console.log(usersInfos);
}, [userData])
return (
<div className='userprofile'>
<button onClick={() => { getUserData() }}> Fetch Random User </button>
<button onClick={() => { console.log(usersInfos) }}> log usersInfos </button>
{usersInfos.map((user, idx) => {
<ProfileCard userData={user} key={idx} />
})}
</div>
)
}
I'm assuming it's something to do with the state batch updating, but I'm not sure. What would be the best practice way of doing this?
I am console logging with the button at the bottom, after the page has loaded.
When userData is initialized to (), I get console log: (2) [undefined, {…}]
When userData is initialized to (0), I get console log: (2) [0, {…}].
When userData is initialized to ([]), i get console log:(2) [Array(0), {…}]
Thanks
When the 2nd effect runs for the first time, userData is undefined because the network request isn't resolved yet. You could simply run the effect only when userData is not undefined.
useEffect(() => {
if (userData) {
const newInfos = [
...usersInfos,
userData,
]
setUsersInfos(newInfos);
}
}, [userData])
However, if the code presented in the question is almost complete, I would use the code below instead. In this way I can update both states with a single effect.
The code below is a snippet from the CodeSandbox fully functional example.
export default function UserProfile() {
const [userData, setUserData] = useState();
const [usersInfos, setUsersInfos] = useState([]);
const getUserData = useCallback(async () => {
let ud = await fetchUserData();
setUserData(ud);
setUsersInfos((prevInfos) => [...prevInfos, ud]);
}, []);
useEffect(() => {
getUserData();
}, [getUserData]);
return (
<div className="userprofile">
<button onClick={getUserData}>Fetch Random User</button>
<button
onClick={() => {
console.log(usersInfos);
}}
>
log usersInfos
</button>
{userData && (
<p>
Current user: {userData.name.first} {userData.name.last}
</p>
)}
<ul>
{usersInfos.map((user, idx) => (
<ProfileCard userData={user} key={idx} />
))}
</ul>
</div>
);
}
If you try to log userInfos just after the setter, it will log the previous state because the function inside the effect is a closure and it captures the value of the state when the effect runs. Since React state is immutable the state update does not works like an assignment but it will be updated at the next render.

Why my react app rendered twice and give me an empty array in first render when I'm trying fetching API?

The second render was success but the side effect is my child component's that using context value from its parent as an initialState (using useState hooks) set its initial state to empty array. I'm using React Hooks ( useState, useContext, useReducer, useEffect)
App.js:
...
export const MainContext = React.createContext();
const initialState = {
data: [],
};
const reducer = (state, action) => {
switch (action.type) {
case "ADD_LIST":
return { ...state, data: action.payload };
default:
return state;
}
};
function App() {
const [dataState, dispatch] = useReducer(reducer, initialState);
const fetchData = () => {
axios
.get("http://localhost:3005/data")
.then((response) => {
const allData = response.data;
if (allData !== null) {
dispatch({ type: "ADD_LIST", payload: allData });
}
})
.catch((error) => console.error(`Error: ${error}`));
};
useEffect(() => {
fetchData();
}, []);
console.log("useReducer", dataState); //LOG 1
return (
<div>
<MainContext.Provider
value={{ dataState: dataState, dataDispatch: dispatch }}
>
<MainPage />
</MainContext.Provider>
</div>
);
}
export default App;
My child component
MainPage.jsx
...
function MainPage() {
const mainContext = useContext(MainContext);
const data = mainContext.dataState.data;
const uniqueList = [...new Set(data.map((list) => list.type))];
console.log("uniqueList", uniqueList); // LOG 2
const [uniqueType, setUniqueType] = useState(uniqueList);
console.log("uniqueType State", uniqueType); // LOG 3
const catAlias = {
account: "Account",
commandLine: "Command",
note: "Note",
bookmark: "Bookmark",
};
return (
// jsx code, not necessary with the issue
)
};
The result :
result image
As you can see, uniqueType state is still empty eventhough the uniqueList is already with filled array. My goal is to make uniqueType initial state into the uniqueList from the first render.
That's the expected behavior. The value used for state initialization is only used in first render. In subsequent renders, the component needs to use useEffect to keep track of value changes.
UseEffect(()=>{
// unique list update.
}, [data]);

React hooks array passing in number when passed into props of component

I am currently working on a chat application and for some reason every time I pass in my array of messages as a prop to another component it passes in a number to the component instead of the message object. I have tried a lot of different methods of passing it in regarding using multiple components etc but it seems to still be passing in the number of elements for some reason. Any help is appreciated... code is below
Component receiving the props
import React, { useEffect } from 'react'
import Message from '../../Message/Message'
function Messages({ messages }) {
useEffect(() => {
console.log(messages)
}, [messages])
return (
<div>
test
</div>
)
}
export default Messages
// Import React dependencies.
import React, { useEffect, useState, } from "react";
// Import React dependencies.
import io from 'socket.io-client'
import axios from 'axios'
import Messages from './Messages/Messages'
import uuid from 'react-uuid'
import { Redirect } from 'react-router-dom'
// Import the Slate components and React plugin.
const ENDPOINT = 'http://localhost:5000/'
export const socket = io.connect(ENDPOINT)
const LiveChatFunction = ({ group_id }) => {
// Add the initial value when setting up our state.
const [message, setValue] = useState("")
const [user, setUser] = useState("")
const [groupId, setGroup] = useState('')
const [messages, setMessages] = useState([])
const [toLogin, userAuth] = useState(false)
useEffect(() => {
setGroup(group_id)
axios.post('http://localhost:5000/api/users/refresh_token', null, { withCredentials: true }).then(data => {
if (!data.data.accessToken) {
userAuth(true)
}
})
axios.get('http://localhost:5000/api/users/userInfo', { withCredentials: true }).then(data => {
setUser(data.data.user)
})
socket.on(`message-${group_id}`, data => {
setMessages(messages.push(data))
});
axios.get(`http://localhost:5000/live/${group_id}`).then(x => {
console.log(x.data)
})
}, [group_id, messages])
function setClick() {
const data = {
messageId: uuid(),
user,
groupId,
message
}
socket.emit('message', data)
}
if (toLogin) {
return (
<Redirect to="/login" />
)
}
return (
<div>
<input placeholder="message" type="text" onChange={value => {
setValue(value.target.value)
socket.emit('typing-message', { username: user, time: new Date() })
}} />
<button onClick={setClick}>Submit</button>
<Messages messages={messages} />
</div>
)
}
export default LiveChatFunction;
I have added some comments of what I think you can change:
useEffect(() => {
const recieveFunction = (data) => {
//using callback so no dependency on messages
setMessages((messages) => messages.push(data));
};
async function init() {
//next line is pointless, this runs when group_id
// has changed so something must have set it
// setGroup(group_id);
await axios //not sure if this should be done before listening to socket
.post(
'http://localhost:5000/api/users/refresh_token',
null,
{ withCredentials: true }
)
.then((data) => {
if (!data.data.accessToken) {
userAuth(true);
}
});
await axios
.get('http://localhost:5000/api/users/userInfo', {
withCredentials: true,
})
.then((data) => {
setUser(data.data.user);
});
//start listening to socket after user info is set
socket.on(`message-${group_id}`, recieveFunction);
axios
.get(`http://localhost:5000/live/${group_id}`)
.then((x) => {
console.log(x.data);
});
}
init();
//returning cleanup function, guessing socket.off exists
return () =>
socket.off(`message-${group_id}`, recieveFunction);
}, [group_id]); //no messages dependencies
console.log('messages are now:',messages);
If messages is still not set correctly then can you log it
So I think I found your problem:
In your useEffect hook, you're setting messages to the wrong thing.
socket.on(`message-${group_id}`, data => {
setMessages(messages.push(data))
});
An example:
const m = [].push();
console.log(m);
// m === 0
const n = [].push({});
console.log(n);
// n === 1
As you can see this is the index.
So what you need is:
socket.on(`message-${group_id}`, data => {
messages.push(data);
setMessages(messages);
});
This will set messages to the array of messages.

Categories

Resources