I'm trying to make sort of a twitter clone using react and in the Feed component, I am not able to render the tweets for some reason.
This is Home, the parent component of Feed. this is also where I call most of the relevant info from firebase.
function Home() {
const[userInfo, setUserInfo] = useState({})
const {user} = useUserAuth()
useEffect(() => {
const getUser = async()=>{
const docRef = doc(db, "users", auth.currentUser.uid);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
setUserInfo(docSnap.data())
} else {
// doc.data() will be undefined in this case
}}
getUser()
},[user]);
return (
<div className='cont-all'>
<div className='menu'>
<IconContext.Provider value={{className:"react-icons"}}>
<ul>
<li className='menu-option'><GrTwitter color='rgb(14, 147, 255)' size={30}/></li>
<li className='menu-option'><div className='hover-wrap'><AiFillHome size={23} ></AiFillHome>{window.innerWidth > 1200? "Home":null}</div></li>
<li className='menu-option'><div className='hover-wrap'><BiMessageAltDetail size={23}></BiMessageAltDetail>{window.innerWidth > 1200? "Messages":null}</div></li>
<li className='menu-option'><div className='hover-wrap'><AiOutlineUser size={23}></AiOutlineUser>{window.innerWidth> 1200? "Profile":null}</div></li>
</ul>
</IconContext.Provider>
<SessionInfo user ={userInfo}/>
</div>
<div className='feed'>
<CreateTweet username={userInfo.username} />
{userInfo?<Feed user_info={userInfo}/>:null}
</div>
<div className='trends'>3</div>
</div>
)
}
This is the Feed component, parent of the Tweet components i'm trying to render multiple times depending on the number of posts i have stored.I'm storing the props in state in the first useEffect and the second useEffect is where I call all the tweets from firebase
function Feed({ user_info }) {
const [tweets, setTweets] = useState([]);
const [user,setUser] = useState({})
useEffect(()=>{
setUser(user_info)
},[user_info])
useEffect(() => {
const tweets_arr = []
if (user.posts!==undefined && user.posts!==[]){
const call = async (post)=>{
const docRef = doc(db, "Twoots", post);
const wanted_doc = await getDoc(docRef);
tweets_arr.push(wanted_doc.data())
}
user.posts.forEach(element => {
call(element)
});
setTweets(tweets_arr)
}
},[user]);
return (
<div>
{tweets.map(item =>{return <Tweet item={item} />})}
</div>
);
}
Finally this is the Tweet component. I don't think it's too relevant as it just receives data and displays it but i'll leave it here anyway
function Tweet(props) {
return (
<div className='tweet'>
<div className='wrap-pfp-twt'>
<div className='tw-pfp-cont1'><img src='https://conteudo.imguol.com.br/c/esporte/96/2021/11/29/lionel-messi-atacante-do-psg-1638213496667_v2_4x3.jpg' className='tw-pfp' alt=""></img></div>
<div>
<div className='author-id'>#{props.item.username} . {props.item.likes}</div>
<div className='actual-tweet'>{props.item.body}</div>
</div>
</div>
<div className='interaction-bar'>
<FaRegComment/><BsShare/><AiOutlineHeart/>
</div>
</div>
)
}
Your tweets array is always going to be empty.
If you formatted your code correctly, this is what you'd see:
useEffect(() => {
const tweets_arr = []
if (user.posts!==undefined && user.posts!==[]){
const call = async (post)=> {
const docRef = doc(db, "Twoots", post);
const wanted_doc = await getDoc(docRef);
tweets_arr.push(wanted_doc.data())
}
user.posts.forEach(element => {
call(element)
});
setTweets(tweets_arr)
}
}, [user]);
Your problem is that the call() function is async. When you do a .forEach it does not wait for the async function to complete, so it immediately calls setTweets(tweets_arr). And since the .forEach has not completed, you will never see the tweets
Related
I want to display a list of products based on specific categories fetched from api, like below:
const API = "https://dummyjson.com/products";
const ProductsList = () => {
const { cate } = useParams(); //here I am getting category from Viewall component
const { getFilterProducts, filter_products } = useFilterContext();
useEffect(() => {
getFilterProducts(`${API}/category/${cate}`);
}, [cate]);
return (
<div className="mx-2 mt-2 mb-16 md:mb-0 grid grid-cols-1 md:grid-cols-12">
<div className="h-9 w-full md:col-span-2">
<FilterSection />
</div>
<div className="md:col-span-10">
<ProductListDetails products={filter_products} />
</div>
</div>
);
};
My FilterContextProvider is as follows
const initialState = {
filter_products: [],
};
const FilterProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const { products } = useAppContext();
const getFilterProducts = async (url) => {
dispatch({ type: "FILTERS_LOADING" });
try {
const res = await fetch(url);
const data = await res.json();
if (!res.ok) {
var error = new Error("Error" + res.status + res.statusText);
throw error;
}
dispatch({ type: "LOAD_FILTER_PRODUCTS", payload: data.products });
} catch (err) {
dispatch({ type: "FILTERS_ERROR", payload: err.message });
}
};
return (
<FilterContext.Provider value={{ ...state, getFilterProducts }}>
{children}
</FilterContext.Provider>
);
};
I tried using this simple approach in my ProductList component to clean up:
useEffect(() => {
let inView = true;
getFilterProducts(`${API}/category/${cate}`);
return () => {
inView = false;
};
}, [cate]);
But it does not seem to work. When I move to the ProductList component, it first displays data of my previous filer_products value, then after a few fractions of seconds, updates the data and shows current data.
I am expecting that when the ProductList component unmounts, its rendered data should vanish, and when I navigate it again, it should render the current data directly, not after a fraction of seconds.
As you explained, I assume your context is wrapping your routes, and it's not re-rendering when switching between pages. A simple solution is to have a loader in ProductsList, wait for the new data to replace the old, and have the user notice what's happening with a loader:
const ProductsList = () => {
const { cate } = useParams(); //here I am getting category from Viewall component
const { getFilterProducts, filter_products } = useFilterContext();
const [loading, setLoading] = useState(true);
useEffect(() => {
getFilterProducts(`${API}/category/${cate}`).then(() => {
setLoading(false);
});
}, [cate]);
if (loading) {
return <p>Hang tight, the data is being fetched...</p>;
}
return (
<div className="mx-2 mt-2 mb-16 md:mb-0 grid grid-cols-1 md:grid-cols-12">
<div className="h-9 w-full md:col-span-2">
<FilterSection />
</div>
<div className="md:col-span-10">
<ProductListDetails products={filter_products} />
</div>
</div>
);
};
If you need to clear your store in a clean-up function, you can add dispatch as part of your context value, grab it in ProductsList and call it like so:
<FilterContext.Provider value={{ ...state, getFilterProducts, dispatch }}>
{children}
</FilterContext.Provider>
const { getFilterProducts, filter_products, dispatch } = useFilterContext();
useEffect(() => {
getFilterProducts(`${API}/category/${cate}`);
return () => {
dispatch({ type: "LOAD_FILTER_PRODUCTS", payload: {} });
};
}, [cate]);
In Firebase I have two Documents. one is "users" and "posts".
Inside posts, I have added an id equal to the "users" collection id.
What I'm trying to achieve is when I'm getting posts I need to get post-added user data.
I'm getting all the posts but I cannot get related user data. Can anyone help me do this?
import {
collection,
query,
onSnapshot,
orderBy,
doc,
getDoc,
} from "firebase/firestore";
import { db } from "../../FireBaseConfig";
export default function Home() {
const [posts, setPosts] = useState();
const getPostUser = async (userID) => {
try {
const userQuery = doc(db, "users", userID);
const userRef = await getDoc(userQuery);
const userData = userRef.data();
return userData;
} catch (err) {
console.log(err);
}
};
useEffect(() => {
const getPosts = async () => {
try {
const q = query(collection(db, "posts"), orderBy("statusDate", "desc"));
const unsubscribe = onSnapshot(q, (querySnapshot) => {
let posts_arr = [];
let adsfadsfad = "";
querySnapshot.forEach(async (item, index, array) => {
getPostUser(item.data().id)
.then((res) => {
adsfadsfad.push(res);
})
.catch((err) => {
console.log(err);
});
console.log(adsfadsfad);
posts_arr.push(
<div className="home__posts--post" key={item.id}>
<div className="home__posts--post-user flex items-center">
{/* <img
src={abc["avatar"]}
alt={abc["avatar"]}
className="w-[50px] h-[50px] rounded-full"
/>
<h4>{abc["avatar"]}</h4> */}
{/* {asdasd} */}
</div>
<div>
{item.data().statusImage && (
<img
src={item.data().statusImage}
alt={item.data().status}
/>
)}
</div>
</div>
);
});
setPosts(posts_arr);
});
} catch (err) {
console.log(err);
}
};
getPosts();
}, []);
return (
<div className="home p-4 max-w-screen-sm mx-auto">
<div className="home__posts">{posts}</div>
</div>
);
}
Your code also queries for user data for every post. In case all the posts returned by first query are posted by same person, it'll be redundant. Also the getDoc() (getPostUser()) returns a Promise so you need to handle that as well. Try refactoring the code as shown below:
useEffect(() => {
const getPosts = async () => {
const q = query(collection(db, "posts"), orderBy("statusDate", "desc"));
const unsubscribe = onSnapshot(q, async (querySnapshot) => {
const posts = querySnapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
// Get an array of unique user IDs from all posts
const userIds = [...new Set(posts.map((post) => post.userId))];
// Fetch documents of those users only
const userPromises = userIds.map((userId) =>
getDoc(doc(db, "users", userId))
);
const userDocs = await Promise.all(userPromises);
// Record<string, UserDocumenData>
const users = userDocs.reduce((acc, userDoc) => {
acc[userDoc.id] = userDoc.data();
return acc;
}, {});
// use "posts" array and "users" object to render your UI
// You can get user info as shown below
// const postUser = users[posts[0].userId]
});
};
getPosts();
}, []);
If you want to, you can also additionally remove some userId from the second query in case their data has been fetched already.
Here I am trying to get productList from MySQL database and for each product object I am assigning new property - imageURL via getImages(). When I log productList to console, there is property imageURL with correct url. Problem is, when I try to map it, it shows nothing. Why?
const storageRef = firebase.storage().ref("/assets")
const [productList, setProductList] = useState([])
useEffect(() => {
Axios.get("http://localhost:3001/product/get").then((response) => {
setProductList(response.data)
})
}, [])
useEffect(() => {
getImages(productList)
}, [productList])
const getImages = (array) => {
array.forEach((item) => {
storageRef.child(`${item.bannerImage}`).getDownloadURL().then((url) => {
item.imageURL = url
})
})
}
My map function:
{productList.map((val) => {
return (
<div key={val.id} className="product">
<div className="item">
<h1>Product title: {val.title}</h1>
<h2>Price: {val.price}</h2>
<h2>Quantity: {val.quantity}</h2>
<h2>IMAGE: {val.imageURL}</h2>
</div>
</div>
)
})}
Problems:
You are not setting productList back in getImages function. You are just iterating over array.
getDownloadURL is a async function, you should not use it inside loop. The best way to do this is through a recursive function. But you can also do this as below:
Solution
Your getImage function
const getImage = async (bannerImage) => {
const url = await storageRef.child(bannerImage).getDownloadURL();
return url;
}
then your map function
{productList.map((val) => {
return (
<div key={val.id} className="product">
<div className="item">
<h1>Product title: {val.title}</h1>
<h2>Price: {val.price}</h2>
<h2>Quantity: {val.quantity}</h2>
<h2>IMAGE: {getImage(val.bannerImage)}</h2>
</div>
</div>
)
})}
I would suggest you create another small component for your image rendering and handle async for getDownloadURL behaviour inside that component
function ProductImage({bannerImage}) {
const [imageUrl, setImageUrl] = useState('')
useEffect(() => {
async function getImage(bannerImage) {
const url = await bannerImage.getDownloadURL()
setImageUrl(url)
}
getImage(bannerImage)
}, [bannerImage])
return imageUrl ? <h2>IMAGE: {imageUrl}</h2> : '...Loading'
}
And use this component in your main component
{productList.map((val) => {
return (
<div key={val.id} className="product">
<div className="item">
<h1>Product title: {val.title}</h1>
<h2>Price: {val.price}</h2>
<h2>Quantity: {val.quantity}</h2>
<ProductImage bannerImage={val.bannerImage} />
</div>
</div>
)
})}
I am building a chat app and trying to match the id params to render each one on click.I have a RoomList component that maps over the rooms via an endpoint /rooms
I then have them linked to their corresponding ID. THe main components are Chatroom.js and RoomList is just the nav
import moment from 'moment';
import './App.scss';
import UserInfo from './components/UserInfo';
import RoomList from './components/RoomList';
import Chatroom from './components/Chatroom';
import SendMessage from './components/SendMessage';
import { Column, Row } from "simple-flexbox";
import { Route, Link, Switch } from 'react-router-dom'
function App() {
const timestamp = Date.now();
const timeFormatted = moment(timestamp).format('hh:mm');
const [username, setUsername] = useState('');
const [loggedin, setLoggedin] = useState(false);
const [rooms, setRooms] = useState([]);
const [roomId, setRoomId] = useState(0);
const handleSubmit = async e => {
e.preventDefault();
setUsername(username)
setLoggedin(true)
};
useEffect(() => {
let apiUrl= `http://localhost:8080/api/rooms/`;
const makeApiCall = async() => {
const res = await fetch(apiUrl);
const data = await res.json();
setRooms(data);
};
makeApiCall();
}, [])
const handleSend = (message) => {
const formattedMessage = { name: username, message, isMine: true};
}
return (
<div className="App">
<Route
path="/"
render={(routerProps) => (
(loggedin !== false) ?
<Row>
<Column>
{/*<Chatroom roomId={roomId} messages={messages} isMine={isMine}/>*/}
</Column>
</Row>
:
<form onSubmit={handleSubmit}>
<label htmlFor="username">Username: </label>
<input
type="text"
value={username}
placeholder="enter a username"
onChange={({ target }) => setUsername(target.value)}
/>
<button type="submit">Login</button>
</form>
)}
/>
<Switch>
<Route
exact
path="/:id"
render={(routerProps) => (
<Row>
<Column>
<UserInfo username={username} time={timeFormatted}/>
<RoomList rooms={rooms}/>
</Column>
<Column>
<Chatroom {...routerProps} roomId={roomId}/>
<SendMessage onSend={handleSend}/>
</Column>
</Row>
)}
/>
</Switch>
</div>
);
}
export default App;
RoomList.js
import { Row } from "simple-flexbox";
const RoomList = (props) => {
return (
<div className="RoomList">
<Row wrap="false">
{
props.rooms.map((room, index) => {
return (
<Link to={`/${room.id}`} key={index}>{room.id} {room.name}</Link>
)
})
}
</Row>
</div>
)
}
export default RoomList;
Chatroom.js
this is the main component that should render based on the ID
import Message from './Message';
import { Link } from 'react-router-dom'
const Chatroom = (props) => {
const [roomId, setRoomId] = useState(0);
const [name, setName] = useState('Roomname')
const [messages, setMessages] = useState([]);
useEffect(() => {
let apiUrl= `http://localhost:8080/api/rooms/`;
const id = props.match.params.id;
const url = `${apiUrl}${id}`;
const makeApiCall = async () => {
const res = await fetch(url);
const data = await res.json();
setRoomId(data.id);
setUsers(data.users)
setName(data.name)
};
makeApiCall();
}, []);
useEffect(() => {
const id = props.match.params.id;
const url = `http://localhost:8080/api/rooms/${id}/messages`;
const makeApiCall = async() => {
const res = await fetch(url);
const data = await res.json();
setMessages(data);
};
makeApiCall();
}, [])
return (
<div className="Chatroom">
{name}
</div>
)
}
export default Chatroom;```
when I click on the links I want the change to refresh the new content but it wont? any ideas why ? thank you in advance!
Notice that your functional component named App does not have any dependencies and that is fine since data should just be fetched once, on mount. However, on ChatRoom we want a new fetch everytime that roomId changes.
First thing we could do here is adding props.match.params.id directly into our initial state.
const [roomId, setRoomId] = useState(props.match.params.id); // set up your initial room id here.
Next we can add an effect that checks if roomId needs updating whenever props change. Like this:
useEffect(()=>{
if(roomId !== props.match.params.id) {
setRoomId(props.match.params.id)
}
}, [props])
Now we use roomId as our state for the api calls and add it in the brackets (making react aware that whenever roomId changes, it should run our effect again).
useEffect(() => {
let url = "http://localhost:8080/api/rooms/" + roomId; // add room id here
const makeApiCall = async () => {
const res = await fetch(url);
const data = await res.json();
setUsers(data.users)
setName(data.name)
};
makeApiCall();
}, [roomId]); // very important to add room id to your dependencies as well here.
useEffect(() => {
const url = `http://localhost:8080/api/rooms/${roomId}/messages`; // add room id here as well
const makeApiCall = async() => {
const res = await fetch(url);
const data = await res.json();
setMessages(data);
};
makeApiCall();
}, [roomId]) // very important to add room id to your dependencies as well here.
I believe that it should work. But let me build my answer upon this:
When mounted, meaning that this is the first time that the ChatRoom is rendered, it will go through your useEffect and fetch data using roomId as the initial state that we setup as props.match.params.id.
Without dependencies, he is done and would never fetch again. It would do it once and that's it. However, by adding the dependency, we advise react that it would watch out for roomId changes and if they do, it should trigger the function again. It is VERY IMPORTANT that every variable inside your useEffect is added to your brackets. There is eslint for it and it is very useful. Have a look at this post. It helped me a lot.
https://overreacted.io/a-complete-guide-to-useeffect/
Let me know if it works and ask me if there is still doubts. =)
I call a get request to my api, and then register them to my state with this:
useEffect(() => {
fetchPosts()
},)
const [posts, setPosts] = useState([])
const fetchPosts = async () => {
const data = await fetch('http://localhost:3000/posts/')
const posts_data = await data.json()
setPosts(posts_data)
}
I even tried the axios approach:
await axios.get('http://localhost:3000/posts/')
.then(res => {
setPosts(res.data)
console.log(posts)
})
If I console.log posts_data and posts, it gives me the Object I got from my api:
[{title: "Sample post", desc: "sample desc"}, {...}]
But whenever I iterate and display it:
<div>
{posts.map(post => {
<div>
<p>{post.title}</p>
<h1>asdjasdljaskldjs</h1>
</div>
})}
</div>
It doesn't show up on the page. I even tried adding that random string there asdjasdljaskldjs and it doesn't show too. The data is received and stored, but I wonder why it doesn't display.
Entire component code
import React, {useState, useEffect} from 'react'
import axios from 'axios'
function Posts() {
useEffect(() => {
fetchPosts()
},)
const [posts, setPosts] = useState([])
const fetchPosts = async () => {
await axios.get('http://localhost:3000/posts/')
.then(res => {
setPosts(res.data)
console.log(posts)
})
// const data = await fetch('http://localhost:3000/posts/')
// const posts_data = await data.json()
// setPosts(posts_data)
// console.log(posts)
}
return (
<div className="container-fluid col-lg-7 mt-3">
<h1>POSTS</h1>
<div>
{posts.map(post => {
<div>
<p>{post.title}</p>
<h1>asdjasdljaskldjs</h1>
</div>
})}
</div>
</div>
)
}
export default Posts
I also noticed when I console.log the posts_data or posts, it keeps printing over and over again while you're on the page. Is that normal?
Your mapping function isn't returning the JSX. Change your return to:
return (
<div className="container-fluid col-lg-7 mt-3">
<h1>POSTS</h1>
<div>
{posts.map(post => (
<div>
<p>{post.title}</p>
<h1>asdjasdljaskldjs</h1>
</div>
))}
</div>
</div>
)
You need to surround the returned JSX with parens, not {}, or you need a return before the {}.