I had previously completed the code for infinite scrolling using the concept of pages. Now, I'm working on code that retrieves 5 messages older than the msgDate of the last element in the message array, rather than using the page concept, from the backend server, but it's not working.
Here is the original code that was written using the page concept for infinite scrolling:
"use client";
import { Typography } from "@mui/material";
import axios from "axios";
import { useEffect, useState, useRef, useCallback } from "react";
const TOTAL_PAGES = 100;
const options = {
threshold: 0.1,
};
interface ChatMessage {
channelIdx: number;
sender: string;
msg: string;
}
const Chats = () => {
const [loading, setLoading] = useState(true);
const [pageNum, setPageNum] = useState(0);
const [chats, setChats] = useState<ChatMessage[]>([]);
const observerTarget = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0]?.isIntersecting) {
setPageNum((num) => num + 1);
}
}, options);
if (observerTarget.current) {
observer.observe(observerTarget.current);
}
return () => {
if (observerTarget.current) {
observer.unobserve(observerTarget.current);
}
};
}, [observerTarget]);
const callUser = useCallback(async () => {
console.log(pageNum);
await axios
// dev original
.get(`http://localhost:4000/chat/messages?channelIdx=1&index=${pageNum}`)
// haryu's server
// .get(`http://paulryu9309.ddns.net:4000/chat/messages?channelIdx=1&index=${pageNum}`)
.then((res) => {
const newData = Array.isArray(res.data) ? res.data : [res.data];
setChats((prevChats) => [...prevChats, ...newData]);
setLoading(false);
});
}, [pageNum]);
useEffect(() => {
if (pageNum <= TOTAL_PAGES) {
setTimeout(() => {
callUser();
}, 500);
setLoading(true);
}
}, [pageNum]);
return (
<>
<div
style={{
display: "flex",
flexDirection: "column-reverse",
overflowY: "auto",
overflowAnchor: "none",
position: "sticky",
margin: "1% 0% 1% 0%",
padding: "2% 2% 0.5% 2%",
height: "36vh",
}}
>
{chats.map((chats, i) => {
return (
<div
key={i}
style={{
listStyleType: "none",
margin: "0px 0 0 0",
color: "white",
padding: 0,
}}
>
<Typography variant="h6">
{chats.sender + ": " + chats.msg}
</Typography>
</div>
);
})}
<div ref={observerTarget}></div>
{loading === true && <p>loading...</p>}
</div>
</>
);
};
export default Chats;
down below is code that I want to implement for msgDate...
The file structure is as follows. Under ChatWindow.tsx
, DmChat.tsx
and BottomField.tsx
are placed side by side as children. In BottomField, you can send messages, and when you send a message to the server, DmChat receives messages from the server and displays them on the screen.
In the server, when requesting previous chat records, The Server also request the msgDate. The Server compare it with the msgDate stored in the database and emit the previous 5 messages that are older than the received msgDate.
I implemented infinite scrolling in reverse using the IntersectionObserver API. When isIntersection is true, I implemented the functionality to load the previous 5 messages from the server. However, the callHistory function is not being called.
When checking the back-end server, it appears that there were no incoming requests from the client for requesting previous chat records.
I've been struggling with this issue for a long time. Please help me.
below image is part of database. the important thing is msgDate
// - ChatWindow.tsx
"use client";
export interface IChat {
channelIdx: number | undefined;
senderIdx: number | undefined;
sender?: string;
msg: string;
msgDate: string;
}
const ChatWindow = () => {
const [msgs, setMsgs] = useState<IChat[]>([]);
return (
<Box sx={{ margin: "0", padding: "0", height: "60vh", minWidth: "300px" }}>
<ChatField msgs={msgs} setMsgs={setMsgs} />
<BottomField setMsgs={setMsgs} />
</Box>
);
};
export default ChatWindow;
"use client";
interface IPayload {
channelIdx: number | undefined;
senderIdx: number;
msg: string;
targetIdx?: number | null;
}
interface Props {
setMsgs: Dispatch<SetStateAction<IChat[]>>;
}
const BottomField = ({ setMsgs }: Props) => {
const [msg, setMsg] = useState<string>("");
const inputRef = useRef<HTMLInputElement>(null);
const { roomState } = useRoom();
const changeMsg = (event: React.ChangeEvent<HTMLInputElement>) => {
setMsg(event.target.value);
};
useEffect(() => {
const messageHandler = (chat: IChat) => {
setMsgs((prevChats: any) => [...prevChats, chat]);
setMsg("");
};
socket.on("chat_send_msg", messageHandler);
return () => {
socket.off("chat_send_msg", messageHandler);
};
}, []);
useEffect(() => {
inputRef.current?.focus();
}, []);
const onSubmit = useCallback(
(event: React.FormEvent) => {
event.preventDefault();
const payload = {
channelIdx: roomState.currentRoom?.channelIdx,
senderIdx: 98029,
msg: msg,
};
socket.emit("chat_send_msg", payload);
inputRef.current?.focus();
},
[msg]
);
return (
<Box
sx={{
marginBottom: 0,
backgroundColor: "#4174D3",
display: "flex",
justifyContent: "center",
margin: "0.5% 2% 2% 2%",
borderRadius: "10px",
minWidth: "260px",
}}
>
<Box sx={{ display: "flex" }}>
<Box component="form" noValidate autoComplete="off" onSubmit={onSubmit}>
<FormControl>
<OutlinedInput
style={{
backgroundColor: "#1e4ca9",
height: "5%",
width: "45vw",
margin: "8px",
color: "white",
marginTop: "3%",
}}
autoFocus
ref={inputRef}
value={msg}
onChange={changeMsg}
placeholder="Please enter message"
inputProps={{
style: {
height: "10px",
},
}}
onKeyPress={(event) => {
if (event.key === "Enter") {
onSubmit(event);
}
}}
/>
</FormControl>
</Box>
<Button
style={{
width: "8.5vw",
justifyContent: "center",
alignItems: "center",
verticalAlign: "middle",
margin: "2.5% 0 2.5% 0",
}}
variant="contained"
onClick={onSubmit}
>
Send
</Button>
</Box>
</Box>
);
};
export default BottomField;
// - DmChat.tsx
"use client";
import { Box, Typography } from "@mui/material";
import { useRoom } from "@/context/RoomContext";
import { useCallback, useEffect, useRef, useState } from "react";
import { Dispatch, SetStateAction } from "react";
import { IChat } from "../ChatWindow";
import axios from "axios";
interface Props {
msgs: IChat[];
setMsgs: Dispatch<SetStateAction<IChat[]>>;
}
const DmChat = ({ msgs, setMsgs }: Props) => {
const [loading, setLoading] = useState<boolean>(true);
const { roomState } = useRoom();
const observerTarget = useRef(null);
const [lastDate, setLastDate] = useState<string>();
const [signal, setSignal] = useState<boolean>(false);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
setSignal(true);
}
}, {threshold: 0.1})
if (observerTarget.current)
observer.observe(observerTarget.current)
return () => {
if (observerTarget.current) {
observer.unobserve(observerTarget.current)
}
}
}, [observerTarget])
useEffect(() => {
if (signal == true) {
console.log("signal == true,call history")
callHistory();
setLoading(true);
}
}, [signal])
const callHistory = useCallback(async () => {
console.log("callHistory start")
if (!lastDate) {
setLoading(false);
return;
}
await axios
.get(
`http://localhost:4000/chat/messages?channelIdx=${roomState.currentRoom?.channelIdx}&msgDate=${lastDate}`
)
.then((res) => {
const newData = Array.isArray(res.data) ? res.data : [res.data];
setMsgs((prevMsgs) => [...prevMsgs, ...newData]);
setLoading(false);
const lastIdx = msgs.length - 1;
const lastElement = msgs[lastIdx];
setLastDate((prev) => lastElement.msgDate);
});
}, [lastDate, msgs, signal]);
// When sending and receiving messages in a chat,
// it fetches the oldest message's Date from the loaded chat history each time.
useEffect(() => {
if (msgs.length > 0) {
const lastIdx = msgs.length - 1;
const lastElement = msgs[lastIdx];
setLastDate(() => lastElement.msgDate);
console.log("메세지 갱신됐어, 라스트 데이트도 업뎃함");
console.log(Date.now(), lastDate);
}
}, [lastDate, msgs]);
return (
<Box
sx={{
display: "flex",
flexDirection: "column-reverse",
overflowY: "scroll",
overflowAnchor: "none",
position: "sticky",
backgroundColor: "#3272D2",
height: "40vh",
borderRadius: "5px",
listStyleType: "none",
margin: "0% 2% 1% 2%",
}}
>
{msgs.map((value, i) => {
return (
<ul
key={i}
style={{ margin: "1% 0% 1% 0%", padding: "2% 2% 0.5% 2%" }}
>
<div
style={{
listStyleType: "none",
margin: "0px 0 0 0",
color: "white",
padding: 0,
}}
>
{
<Typography variant="h6">
{value.sender + ": " + value.msg}
</Typography>
}
</div>
</ul>
);
})}
<div ref={observerTarget}>this is the target</div>
{loading === true && (
<Typography style={{ color: "white" }} component={"div"}>
loading...
</Typography>
)}
</Box>
);
};
export default DmChat;
I believe I understand your issue, I faced a similar one in Cart Management, the issue is with how you are managing the state variables, you should not be passing setMsgs to other components and specially not updating the state in another component when that variable itself is being used in 2 other components,
you can pass the state variable from top-down but not if you make changes to it in the lower level component it will cause infinite re-renders.
Simple solution is use redux toolkit for state management, because your states are getting overwritten by each other
Your issues: In bottomField you are using "setMsgs((prevChats: any) => [...prevChats, chat])" but what are prevChats? you have not declared or assignmed any value to it You are not passing any date to the server in message, its just a string
Also it looks like you are using ChakraUI?