I have a few chat components, chat (parent), CreateMessage(child), and DisplayMessages(child). All three components will be shown below.
The user creates a message with the CreateMessage component. It saves it in the useState hook, indivMessages, stored in the parent Chat component.
The indivMessages are sent to the DisplayMessages component. It displays the messages as well as groups messages by the same author together based off the user id.
The problem is, the indivMessages state is getting set with the value from formattedMessages, which is only set inside the useEffect hook in DisplayMessages.
Why is indivMessages getting set with the value for formattedMessages??
For the sake of this example, I commented out all of the socket stuff to just work in a sterile environment - the same results will happen both ways.
Chat.js
import React, { useState, useEffect, useContext } from "react";
import { useSelector } from "react-redux";
import { SocketContext } from "src/SocketContext";
import CreateMessage from "./details/CreateMessage";
import DisplayMessages from "./details/DisplayMessages";
export default function Chat(props) {
const state = useSelector((state) => state);
const [indivMessages, setIndivMessages] = useState([]);
const socket = useContext(SocketContext);
// useEffect(() => {
// if (state.chatShow) {
// socket.emit("SUBSCRIBE_CHAT", state.chat.chatRoom);
// return () => {
// socket.emit("UNSUBSCRIBE", state.chat.chatRoom);
// };
// }
// });
// useEffect(() => {
// socket.on("new_message", (data) => {
// setIndivMessages([...indivMessages, data]);
// });
// return () => {
// socket.off("new_message");
// };
// }, [socket, indivMessages]);
return (
<div className="d-flex flex-column h-100 justify-content-end">
<DisplayMessages state={state} indivMessages={indivMessages} />
<CreateMessage
state={state}
indivMessages={indivMessages}
setIndivMessages={setIndivMessages}
/>
</div>
);
}
CreateMessage.js
import React, { useState, useContext } from "react";
import { CInputGroup, CInput, CInputGroupAppend, CButton } from "#coreui/react";
import CIcon from "#coreui/icons-react";
import { SocketContext } from "src/SocketContext";
export default function CreateMessage(props) {
const { indivMessages, setIndivMessages, state } = props;
const [newMessage, setNewMessage] = useState("");
const socket = useContext(SocketContext);
const sendMessage = () => {
let messageTemplate = {
messages: [{ msg: newMessage }],
username: state.user.username,
_id: indivMessages.length + 1,
ownerId: state.user._id,
picture: state.user.picture,
chatRoom: state.chat.chatRoom,
date: Date.now(),
};
// socket.emit("create_message", messageTemplate);
setIndivMessages((msgs) => [...msgs, messageTemplate]);
document.getElementById("msgInput").value = "";
};
return (
<CInputGroup style={{ position: "relative", bottom: 0 }}>
<CInput
type="text"
style={{ fontSize: "18px" }}
id="msgInput"
className="rounded-0"
placeholder="Type a message here..."
autoComplete="off"
onChange={(e) => setNewMessage(e.target.value)}
onKeyUp={(e) => e.code === "Enter" && sendMessage()}
/>
<CInputGroupAppend>
<CButton color="success" className="rounded-0" onClick={sendMessage}>
<CIcon name="cil-send" />
</CButton>
</CInputGroupAppend>
</CInputGroup>
);
}
DisplayMessages.js
import React, { useEffect, useState } from "react";
import { CContainer, CCard, CImg, CCol, CLabel } from "#coreui/react";
export default function DisplayMessages(props) {
const { indivMessages, state } = props;
const [formattedMessages, setFormattedMessages] = useState([]);
useEffect(() => {
//Create Grouped Messesges
let messagesArray = [...indivMessages];
let sortedArray = messagesArray.sort((a, b) => {
return new Date(a.date) - new Date(b.date);
});
let grouped = [];
for (let i = 0; i < sortedArray.length; i++) {
let index = grouped.length - 1;
if (sortedArray[i].ownerId === grouped[index]?.ownerId) {
let lastMessage = grouped.pop();
sortedArray[i].messages[0]._id = sortedArray[i]._id;
lastMessage.messages = [
...lastMessage.messages,
sortedArray[i].messages[0],
];
grouped.push(lastMessage);
} else {
console.log(i, grouped.length);
grouped.push(sortedArray[i]);
}
}
setFormattedMessages(grouped);
}, [indivMessages]);
useEffect(() => {
let msgContainer = document.getElementById("msgContainer");
msgContainer.scrollTop = msgContainer.scrollHeight;
}, [formattedMessages]);
return (
<CContainer
className="mt-2 no-scroll-bar"
style={{ overflow: "auto", maxHeight: "85vh" }}
id="msgContainer"
>
{formattedMessages.map((msg) => {
return (
<CCard
key={msg._id}
className="d-flex flex-row p-2"
color="secondary"
accentColor={state.user._id === msg.ownerId && "primary"}
>
<CImg
src={msg.picture}
alt={msg.owner}
className="w-25 align-self-start rounded"
/>
<CCol>
<CLabel>
<strong>{msg.username}</strong>
</CLabel>
{msg.messages.map((message) => {
return (
<p key={message._id ? message._id : msg._id}>{message.msg}</p>
);
})}
</CCol>
</CCard>
);
})}
</CContainer>
);
}
I am thinking that something is going on within the useEffect hook in the DisplayMessages component. This function executes every time the indivMessages array changes.
It creates grouped messages by checking if the newest message's author (ownerId), is the same as the previous message's author. If they are the same, it extracts all the messages from the message itself, and adds them to the previous message's message key in order to create grouped messages.
This result is what is being set in the indivMessages array, which is unexpected since I do not set indivMessages with grouped messages!
Thanks for your help and thanks for some pointers in the right direction!
In the useEffect code, you're modifying the objects in sortedArray, which are also held in indivMessages. For instance:
sortedArray[i].messages[0]._id = sortedArray[i]._id;
Your code setting up sortedArray just copies the array, not the objects within it. If you want to modify those objects, you need to make copies first. For instance, the example above becomes:
sortedArray[i] = {
...sortedArray[i],
messages: [{...sortedArray[i].messages[0], _id = sortedArray[i]._id}, ...sortedArray[i].messages.slice(1)],
};
...or similar.
You have the same sort of problem with:
lastMessage.messages = [
...lastMessage.messages,
sortedArray[i].messages[0],
];
...but probably want to solve it elsewhere (perhaps by changing grouped.push(sortedArray[i]); to grouped.push({...sortedArray[i]});, but I haven't done a really deep read of that code).
Related
I am having a hard time getting my React App working properly.
The thing is that I tried to use UseEffect hooks only to run side effects in my app and this has brought me some problems.
In this simple component I have a chat that get data from Firebase and is capable of updating the Db. I have no problem with the Firebase side but on the front end, the first render is not able to get me the messages into state properly.
I feel that it has of course something to do with async behaviors.
I will try to explain you the flow of my component :
The message text is kept in a const in state call "inputText"; when the form is submited a const call "numberOfMessageSent" is incremented; I have a UseEffect Hook that has [numberOfMessageSent] in its depedency; so after the first mount of the component and when "NumberOfMessageSent" increments the callback will fire; this callback fires 2 async functions: one to fetch the current discussion from the db and another to create a discussion object or update an existing one into the Db. I have a condition :
"numberOfMessagesSent !== 0 && asyncWarperCreateDiscussionInDb()" in the UseEffect Hook so a new discussion empty discussion won't be created the first this component mount.
My problem is that no discussion is displayed (nor properly fetched and stored into state) BEFORE I send a first message. After I send this first message everything works properly.
Can someone help me to understand this better ?
Thank you very much
here is my code :
import React, { useContext, useEffect, useState } from "react";
import "./card-medium-message.style.scss";
import likeEmpty from "./like-empty.png";
import likeFull from "./like-full.png";
import cancel from "./cancel.png";
import send from "./send.png";
import back from "./back.png";
import { useNavigate, useParams } from "react-router-dom";
import { UsersListContext } from "../../context/usersList-context/users-list-context";
import { UserContext } from "../../context/user-context/user-context";
import {
createDiscussionInDb,
goFetchDiscussionInDb,
goFetchDisscussion,
} from "../../utils/firebase";
const CardMediumMessage = () => {
const params = useParams();
const { usersListCTX } = useContext(UsersListContext);
const { currentUserContext } = useContext(UserContext);
const currentUserClickedOn = usersListCTX.filter(
(user) => user.displayName === params.name
);
console.log(currentUserContext);
console.log(currentUserClickedOn[0]);
const [messages, setMessages] = useState([]);
const [inputText, setInputText] = useState("");
const [numberOfMessagesSent, setNumberOfMessagesSent] = useState(0);
const asyncWarperFetchDiscussionInDb = async () => {
if (currentUserClickedOn[0]) {
const discussion = await goFetchDiscussionInDb(
currentUserContext.displayName,
currentUserClickedOn[0].displayName
);
setMessages(discussion.messages);
}
};
const asyncWarperCreateDiscussionInDb = async () => {
await createDiscussionInDb(
currentUserContext.displayName,
currentUserClickedOn[0].displayName,
inputText
);
resetField();
};
useEffect(() => {
numberOfMessagesSent !== 0 && asyncWarperCreateDiscussionInDb();
asyncWarperFetchDiscussionInDb();
console.log(
"this is written after first render of the component or numberOfMessagesSent was updated"
);
}, [numberOfMessagesSent]);
const messageSubmit = async (e) => {
e.preventDefault();
if (inputText == "") {
return;
}
setNumberOfMessagesSent(numberOfMessagesSent + 1);
};
const textChanged = (e) => {
setInputText(e.target.value);
};
const resetField = () => {
setInputText("");
};
const navigate = useNavigate();
messages && console.log(messages);
return (
<div className="card-medium-warp">
<div className="card-medium-message">
<div className="section1" onClick={() => navigate(-1)}>
<div className="profile-image-outer-circle">
{currentUserClickedOn[0] ? (
<img
src={`https://api.dicebear.com/5.x/micah/svg?seed=${currentUserClickedOn[0].displayName}`}
alt="avatar"
className="profile-image"
/>
) : undefined}
</div>
{currentUserClickedOn[0] ? (
<h2 className="name">{currentUserClickedOn[0].displayName} </h2>
) : undefined}
<div
className="back"
style={{ backgroundImage: `url(${back})` }}
></div>
</div>
<div className="section2">
{messages
? messages.map((messageObject, index) => (
<p
key={index}
className={
messageObject.by === currentUserContext.displayName
? "sender-message"
: "receiver-message"
}
>
{messageObject.message}
</p>
))
: undefined}
</div>
<form className="section3" onSubmit={messageSubmit}>
<input
type="text"
className="input"
placeholder="your message"
onChange={textChanged}
value={inputText}
autoFocus
/>
<div
className="send-message"
style={{ backgroundImage: `url(${send})` }}
></div>
</form>
</div>
</div>
);
};
export default CardMediumMessage;
I think I found the solution so I would like to share it :
My mistake was that I was calling functions that were async in themselves but I didn't chain them in an async/await manner.
This is what I am talking about :
const asyncWarperSequence = async () => {
numberOfMessagesSent !== 0 && (await asyncWarperCreateDiscussionInDb());
await asyncWarperFetchDiscussionInDb();
};
useEffect(() => {
console.log("UseEffect Fired");
asyncWarperSequence();
}, [numberOfMessagesSent]);
I'm trying to show the items I get from my database in the calendar, everything is working fine (maybe not), but in short I got the data from the database with an array and then I converted it to an object (because the calendar only accepts objects), but it doesn't show anything and it doesn't give an error either
import React, { useEffect, useState } from 'react'
import { StyleSheet, View, Text } from 'react-native'
import { LocaleConfig, Agenda } from 'react-native-calendars'
import DateTimePicker from 'react-native-modal-datetime-picker';
import { getAuth } from 'firebase/auth';
import { getDatabase, ref, onValue, set, push, get, child } from 'firebase/database';
const Calendario = () => {
const dbRef = ref(getDatabase());
const data = []
var obj = {}
// getting data from the database
useEffect(() => {
getInDB()
} ,[])
const getInDB = () => {
get(child(dbRef, 'users/' + app.currentUser.uid)).then((snapshot) => {
snapshot.forEach(childsnap => {
let dateD = childsnap.child("date").val()
let titleD = childsnap.child("title").val()
let dtsD = childsnap.child("details").val()
// "yyyy-MM-dd": [{any: "whatever", any2: "whatever"}],
data.push({
[dateD] : [{ title: titleD, details: dtsD }],
});
})
obj = Object.assign({}, ...data)
console.log(obj)
})
}
const renderItem = (item) => {
return(
<View style={styles.itemContainer}>
<Text style={styles.textInf}>{item.title}</Text>
<Text style={styles.textInf}>{item.details}</Text>
</View>
)
}
return (
<>
<Agenda
items={obj}
renderEmptyDate={() => {
return <View />;
}}
renderEmptyData={() => {
return <View />;
}}
selected={new Date()}
minDate={null}
renderItem={renderItem}
markingType="custom"
/>
</>
}
You need to use state and set it or otherwise your component will not be rerendered with the new data.
Furthermore, the Agenda component expects an object. By using data as an array and the spread operator, we won't get the desired result.
You can implement this correctly as follows.
...
const [obj, setObj] = useState({});
...
const getInDB = () => {
get(child(dbRef, 'users/' + app.currentUser.uid)).then((snapshot) => {
const temp = {}
snapshot.forEach(childsnap => {
let dateD = childsnap.child("date").val()
let titleD = childsnap.child("title").val()
let dtsD = childsnap.child("details").val()
Object.assign(temp, {dateD: [{ title: titleD, details: dtsD }]})
})
setObj(temp)
})
}
I have implemented a little snack.
React console logs the data variable before the filter function is finished. I want to load the data into the data variable if the blog id matched the params id. But data variable is console logged before the function is finished.
import React, { useContext, useEffect, useState } from "react";
import { BlogDataContext } from "../contexts/BlogDataContext";
export default function BlogPage({ match }) {
var Id = match.params.id;
const context = useContext(BlogDataContext);
const [Blog, setBlog] = useState();
useEffect(() => {
var data = context.filter((b) => {
return b._id === Id;
});
console.log(data);
setBlog(data);
}, []);
return (
<div style={{ color: "red" }} className="BlogPage">
hello
</div>
);
}
Try to put context in useEffect dependency array:
import React, { useContext, useEffect, useState } from "react";
import { BlogDataContext } from "../contexts/BlogDataContext";
export default function BlogPage({ match }) {
var Id = match.params.id;
const context = useContext(BlogDataContext);
const [Blog, setBlog] = useState();
useEffect(() => {
var data = context.filter((b) => {
return b._id === Id;
});
console.log(data);
setBlog(data);
}, [context]);
return (
<div style={{ color: "red" }} className="BlogPage">
hello
</div>
);
}
I am using React's Context API to share data that most of my components need.
The Context is initially defined, but shortly receives data from the Firebase database (please see IdeaContext.tsx). I define the context in a functional component and the display component, which returns a small card based on the information received.
However, the component doesn't render when I start the development server with Yarn. Instead, in order to get it to render, I have to write console.log('something') inside the display component and then it suddenly re-renders. However, when I refresh the server, it again doesn't render.
How can I make my component render immediately (or at least after the context updates with the data from the database?)
Code:
Context Definition:
import React, { createContext, useEffect, useState } from "react";
import { IdeaContextType, Idea } from "../t";
import {ideasRef} from './firebase'
function getIdeas() {
var arr: Array<Idea> = [];
ideasRef.on('value', (snapshot) => {
let items = snapshot.val()
snapshot.forEach( (idea) => {
const obj = idea.val()
arr.push({
title: obj.title,
description: obj.description,
keyID: obj.keyID
})
console.log(arr)
})
})
return arr
}
const IdeaContextDefaultValues: IdeaContextType = {
ideas: [],
setIdeas: () => {},
};
const IdeaContext = createContext<IdeaContextType>(IdeaContextDefaultValues)
const IdeaContextProvider: React.FC = ({ children }) => {
const [ideas, setIdeas] = useState<Array<Idea>>(
IdeaContextDefaultValues.ideas);
useEffect( ()=> {
console.log('getting info')
setIdeas(getIdeas())
}, [])
useEffect( () => {
console.log('idea change: ', ideas)
}, [ideas])
return (
<IdeaContext.Provider value={{ ideas, setIdeas }}>
{children}
</IdeaContext.Provider>
);
};
Displayer and Card Component
import React, { FC, ReactElement, useContext } from "react";
import IdeaCreator from "./IdeaCreator";
import { IdeaContext } from "./IdeaContext";
import { Idea } from "../t";
import { Link } from "react-router-dom";
const IdeaPost:React.FC<Idea> = ({title, keyID, description}):ReactElement => {
console.log('Received',title,description,keyID)
return (
<div className="max-w-sm rounded overflow-hidden shadow-lg">
<img
className="w-full"
src="#"
alt="Oopsy daisy"
/>
<div className="px-6 py-4">
<div className="font-bold text-xl mb-2"> <Link to={"ideas/" + keyID} key= {keyID}> {title}</Link> </div>
<p className="text-gray-700 text-base">{description}</p>
</div>
</div>
);
};
const IdeaDisplay:FC<any> = (props:any):ReactElement => {
const { ideas, setIdeas } = useContext(IdeaContext)
console.log('Ideas in display: ', ideas)
console.log('test') //This is what I comment and uncommend to get it to show
return (
<div className="flex flex-wrap ">
{ideas.map((idea) => {
console.log(idea)
console.log('Sending',idea.title,idea.description,idea.keyID)
console.log(typeof idea.keyID)
return (
<IdeaPost
title={idea.title}
description={idea.description}
keyID = {idea.keyID}
key = {idea.keyID * 100}
/>
);
})}
</div>
);
};
export default IdeaDisplay;
Solution Code:
import React, { createContext, useEffect, useState } from "react";
import { IdeaContextType, Idea } from "../t";
import {ideasRef} from './firebase'
async function getIdeas() {
var arr: Array<Idea> = [];
const snapshot = await ideasRef.once("value");
snapshot.forEach((idea) => {
const obj = idea.val();
arr.push({
title: obj.title,
description: obj.description,
keyID: obj.keyID,
});
console.log(arr);
});
return arr
}
const IdeaContextDefaultValues: IdeaContextType = {
ideas: [],
setIdeas: () => {},
};
const IdeaContext = createContext<IdeaContextType>(IdeaContextDefaultValues)
const IdeaContextProvider: React.FC = ({ children }) => {
const [ideas, setIdeas] = useState<Array<Idea>>(
IdeaContextDefaultValues.ideas);
useEffect(() => {
console.log("getting info");
const setup = async () => {
const ideas = await getIdeas();
setIdeas(ideas);
};
setup()
}, []);
useEffect( () => {
console.log('idea change: ', ideas)
const updateDatabase = async () => {
await ideasRef.update(ideas)
console.log('updated database')
}
updateDatabase()
}, [ideas])
return (
<IdeaContext.Provider value={{ ideas, setIdeas }}>
{children}
</IdeaContext.Provider>
);
};
export {IdeaContext, IdeaContextProvider}
First of all you would need to use once and not on if you want to get the data only once. If you want to use a realtime listener you could send the setIdeas to your function. Also try to be carefull with async/away calls to the Firebase sdk. Your code could look like this:
import React, { createContext, useEffect, useState } from "react";
import { IdeaContextType, Idea } from "../t";
import { ideasRef } from "./firebase";
async function getIdeas() {
var arr: Array<Idea> = [];
const snapshot = await ideasRef.once("value");
let items = snapshot.val();
snapshot.forEach((idea) => {
const obj = idea.val();
arr.push({
title: obj.title,
description: obj.description,
keyID: obj.keyID,
});
console.log(arr);
});
return arr;
}
const IdeaContextDefaultValues: IdeaContextType = {
ideas: [],
setIdeas: () => {},
};
const IdeaContext = createContext < IdeaContextType > IdeaContextDefaultValues;
const IdeaContextProvider: React.FC = ({ children }) => {
const [ideas, setIdeas] =
useState < Array < Idea >> IdeaContextDefaultValues.ideas;
useEffect(() => {
console.log("getting info");
const getData = async () => {
const ideas = await getIdeas();
setIdeas(ideas);
};
}, []);
useEffect(() => {
console.log("idea change: ", ideas);
}, [ideas]);
return (
<IdeaContext.Provider value={{ ideas, setIdeas }}>
{children}
</IdeaContext.Provider>
);
};
this is my react code here I am getting react poll using API but when I start working on handalchange For POST API request I need (PollId,userId and answer) I am getting userId through { const userId = isAutheticated() && isAutheticated().user._id; } but I do not understand how can I get PollId from my all polls, please help...!
import React, { useState, useEffect } from "react";
import Poll from "react-polls";
import "../../styles.css";
import { isAutheticated } from "../../auth/helper/index";
import { getPolls, postPoll } from "../helper/coreapicalls";
import { useParams } from "react-router-dom";
const MainPoll = () => {
const userId = isAutheticated() && isAutheticated().user._id;
const pollId = useParams();
const id = pollId._Id;
console.log(id);
const [polls, setPoll] = useState([]);
const [error, seterror] = useState(false);
// Setting answers to state to reload the component with each vote
const [pollAnswers, setPollAnswers] = useState([]);
useEffect(() => {
loadPoll();
}, []);
const loadPoll = () => {
getPolls().then((data) => {
if (data.error) {
seterror(data.error);
} else {
setPoll(data);
console.log(data);
}
});
};
// Handling user vote
// Increments the votes count of answer when the user votes
const handalchange = () => {
postPoll();
console.log("hello");
};
return (
<div className="">
<div className="container my-5">
<h1 className="blog_heading my-3">Poll's of the Day</h1>
<div className="row">
{polls.reverse().map((poll, index) => (
<div className="col-lg-4 col-12 poll_border" key={index}>
<Poll
noStorage
question={poll.question}
answers={Object.keys(poll.options).map((key) => {
return {
option: key,
votes: poll.options[key].length,
};
})}
onVote={handalchange}
className="mb-2"
/>
</div>
))}
</div>
</div>
</div>
);
};
export default MainPoll;
my frontend image -
Here I have 5 polls , so I can not get PollId from useParams ...! so how can I get..?
Your component seems to represent list of polls, not any specific poll. So if you have an array of polls instead of one poll, than you have multiple ids instead of the single one.
You can get them by mapping your polls array like that:
const pollIds = polls.map((poll) => poll.id); // or any other prop that stores id