Applying infinite scroll with React and JavaScript - javascript

I am trying to add infinite scroll to my page. Whenever the user scrolls down the page it will call in a jSon that will update the array responsible of showing messages and therefore load new messages.
However if i reach the bottom of the page it will call new messages if i go either up or down with the scroll instead of acting as i intended.
No error messages are shown but i am not really sure why this happens.
The code was based on what I've found here link and the code below is what i have now.
class Messages extends React.Component {
constructor(props) {
super(props);
this.state = {
messagesList: [],
messageCount: 0,
totalMessages: 0,
};
}
isBottom = el => el.getBoundingClientRect().bottom <= window.innerHeight;
componentDidMount() {
document.addEventListener('scroll', this.trackScrolling);
}
trackScrolling = () => {
const wrappedElement = document.getElementById("header");
// The if statement bellow checks if the i am at the bottom of the page and if i have more messages to load, the second information comes in the json
if (this.isBottom(wrappedElement) != null && this.state.messageCount <= this.state.totalMessages) {
fetchAllMessages(10, this.state.messageCount)
.then(({ response }) => {
this.setState({
messagesList: this.state.messagesList.concat(response.messages.content),
messageCount: this.state.messageCount + 1,
});
})
.catch(() => null);
document.removeEventListener('scroll', this.trackScrolling);
}
};
componentWillMount() {
fetchAllMessage(10, this.state.messageCount)
.then(({ response }) => {
this.setState({
messageList: response.message.content,
totalMessage: response.message.totalMessage,
messageCount: this.state.messageCount + 1,
});
})
.catch(() => null);
}
render() {
return (
<div>
// Adds the event listener again
{document.addEventListener('scroll', this.trackScrolling)}
<Canvas >
<Title >Messages</Title>
{this.state.messagesList.map(i => (
<Card id={"header"}>
<CardContent>
<div>
<div className={`icon ${MessageType[i.type].class}`} /> // adds an image
</div>
<div className="cardText" >
<Markdown>
{`${i.message} ${i.date != null ? ` on : ${i.date}` : ''}`}
</Markdown>
</div>
</CardContent>
</Card>
))}
</Canvas>
</div>
);
}
}
export default Messages;
I believe it might be something related to either window.innerHeight or the the fact i reset the scroll listener but i am not really sure.

Related

scrolling to bottom of container ReactJS

So, ive been stuck on this for awhile now and i've tried several solutions from StackOverflow to make this happen but nothing seems to work. It should be as simple as using the useRef hook and scrolling to that component, but it doesnt work, i've tried targeting the DOM after the component finishes loading, i've tried even scrolling into view the bottom of the container after i add a new message.
so, im creating a chat application, and like most chats i want the most recent chat at the bottom of the container, and i want the scroll bar to scroll to that most recent chat. the way i have the container setup is 3 seperate columns where the middle column holds all the information on the page, and the left most column is the navbar. the chats are in the middle column, and extend to the bottom of the viewport, and from there anything that overfills the container is hidden, and has a scroll bar.
I should be doing this inside the useEffect hook, but it doesnt work there either.
can anyone see something i cannot see?
const ChatRoom = () => {
const dispatch = useDispatch();
const { id } = useParams();
const {
selectedChat: { chat, loading, messages },
} = useSelector((state) => state.chat);
const { user } = useSelector((state) => state.auth);
const [message, setMessage] = React.useState("");
useEffect(() => {
// check if the chat is already in the store if not dispatch getChat
if (!chat || chat.id !== id) {
dispatch(getChat(id));
}
scrollToBottom(false);
}, [dispatch, id]);
const updateChatName = () => {
// displays a prompt which gathers the chat name the user wants to change to
// dispatches to update chat action to update the chat name
const newChatName = prompt("Enter new chat name");
if (newChatName) {
dispatch(updateChat({ id, chatName: newChatName }));
window.location.reload();
} else {
dispatch(setAlert("Please enter a chat name", "info"));
}
};
const sendMsg = (e, message) => {
// prevents the page from refreshing when the user presses enter
e.preventDefault();
// trim the message to remove whitespace
const trimmedMessage = message.trim();
// if the message is not empty
if (trimmedMessage) {
// dispatch the send message action
dispatch(sendMessage(trimmedMessage, chat._id));
// get the textarea element and clear it
const textarea = document.getElementsByClassName("messageInput")[0];
textarea.value = "";
// scroll to the bottom of the chat
const c = document.getElementsByClassName("chatContainer")[0];
c.scrollTop({
scrollTop: c.scrollHeight,
animate: true,
duration: "slow",
easing: "easeInOutQuad",
});
// focus the textarea
textarea.focus();
// reset the message
setMessage("");
} else {
// if the message is empty
dispatch(setAlert("Please enter a message", "info"));
}
};
return (
<Container fluid className="chatPageContainer">
<Meta
title={
chat
? chat.chatName
? `Chat | ${chat.chatName}`
: `Chat | ${chat.users[0].firstName}, ${
chat.users[1].firstName
} ${
chat.users.length > 2
? `+ ${chat.users.length - 2} other users`
: ""
}`
: ``
}
/>
<Row>
<span className="titleContainer">
<h1>Chat</h1>
</span>
</Row>
<div className="chatTitleBarContainer">
{chat && (
<>
<TitleBar users={chat.users} user={user} />
<span id="chatName" onClick={updateChatName}>
{chat.chatName
? chat.chatName
: `${chat.users[0].firstName}, ${chat.users[1].firstName} ${
chat.users.length > 3
? `+ ${chat.users.length - 2} other users`
: ""
}`}
</span>
</>
)}
</div>
<div className="mainContentContainer">
<div className="chatContainer">
{loading ? (
<Loader />
) : (
<>
<div className="chatMessages">
{messages &&
messages.map((message, indx) => {
// get the last messasge in the chat, and set the lastSenderId to that message.sender._id
const lastMessage = messages[indx - 1];
let lastSenderId = lastMessage
? lastMessage.sender._id
: null;
return (
<MessageItem
key={message._id}
message={message}
user={user}
nextMessage={messages[indx + 1]}
lastSenderId={lastSenderId}
/>
);
})}
{/* dummy div to scroll to bottom of container */}
</div>
<div id="bottomChatContainer"></div>
</>
)}
<div className="footer">
<textarea
name="message"
className="messageInput"
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
sendMsg(e, message);
}
}}
onChange={(e) => setMessage(e.target.value)}
placeholder="Type a message..."
></textarea>
<FiSend
className="sendMessageButton"
onClick={(e) => sendMsg(e, message)}
/>
</div>
</div>
</div>
</Container>
);
};
async function scrollToBottom(animate) {
const c = document.getElementById("bottomChatContainer");
if (animate) {
c.animate({ scrollTop: c.scrollHeight }, "slow");
} else {
c.scrollTop = c.scrollHeight;
}
}
export default ChatRoom;
---Edit: Revised Solution---
In the map function of the array of messages that returns a MessageItem component, one can make use of a temporary variable(boolean) that tracks whether or not the current element of the messages array is the last one, at which point the bool becomes true. This bool value should be passed as prop to the messageItem component that renders each message element. The code should look something like this:
...
<div>{ // Make sure this has necessary CSS to allow it to scroll in the y-direction
messagesArray.map(message, index) => {
let tempBool = false;
if(last element) tempBool = true
return(
<MessageItem {...props} scrollIntoViewBool={tempBool} />
)
}
}</div>
....
In the MessageItem component, one can assign a ref to each container that renders message details, and using the combination of the ref and the scrollIntoViewBool prop, use the .scrollIntoView method on the current value of the ref whenever the prop is true. But this should be done in a useEffect so that the scrolling happens after the messages have been loaded. The code will look like this:
...
const messageRef = useRef(null);
useEffect(() => {
if(!scrollIntoViewBool) return;
const lastMessage = messageRef.current;
lastMessage.scrollIntoView();
}, [messageRef, scrollIntoViewBool]);
...
return...
<div ref={messageRef}>Message Contents</div>
For more details you can check out this and this GitHub link to the chat component I made for my app
one thing you could do is instead of scrolling down to the bottom of the enclosing div, add a sibling div to the mapped-messages and then scroll it into view every time a new message is posted/fetched. The code will look something like this:
<div className="chatMessages">
{messages.map().....}
<div ref={dummyDiv}></div> /* scroll this into view */
</div>

ReactJS what is the proper way to wait for prop to load to avoid crashing my page?

all,
I am building a local website for myself for stocks. Currently I have a store that communicates with my tomcat instance to get stock market data, this works flawlessly.
on my frontend I am attempting to display my data but sometimes it works, sometimes it does not work and I get an "this child prop does not exist" so this is what I implemented:
try{
cellRend = this.cellRenderer;
columnLen = this.props.selectedStock.Revenue.length;
this.state.isLoading = false
}catch(error){
cellRend = this.cellRendererEmpty;
columnLen = 10;
}
if (this.state.isLoading === true){
return <div>Loading!</div>
}
where cellRenderer is my table, cellRendererEmpty is an empty table.
this kind of works and some times it will just display Loading! forever. so my question is what is the correct way to wait for a prop?
here is my full code:
const dispatchToProps = dispatch => {
return{
getSelStock: (stockId) => dispatch(stockActions.getSelStock(stockId))
};
}
class stockPage extends React.Component{
constructor(props) {
super(props);
this.state = {
columnLen:10,
data:null,
isLoading:true
}
console.log(this.props.isLoading)
this.cellRenderer = this.cellRenderer.bind(this);
this.render = this.render.bind(this);
}
cellRenderer({ columnIndex, key, rowIndex, style }) {
return (
<div className={"app"} key={key} style={style}>
<span></span>
{rowIndex === 0 ? (`${this.props.selectedStock.Revenue[columnIndex].date}`) : (
<span>{`${this.props.selectedStock.Revenue[columnIndex].value}`}</span>
)}
</div>
);
}
cellRendererEmpty({ columnIndex, key, rowIndex, style }) {
return (
<div className={"app"} key={key} style={style}>
{rowIndex === 0 ? (`${columnIndex}`) : (
<span>{`${columnIndex}`}</span>
)}
</div>
);
}
render() {
var cellRend, columnLen
console.log("Hey")
this.props.getSelStock(this.props.match.params.stockId);
try{
cellRend = this.cellRenderer;
columnLen = this.props.selectedStock.Revenue.length;
this.state.isLoading = false
}catch(error){
cellRend = this.cellRendererEmpty;
columnLen = 10;
}
if (this.state.isLoading === true){
return <div>Loading!</div>
}
return(
<div>
<h1>{this.props.match.params.stockId}</h1>
<AutoSizer disableHeight>
{({ width }) => (
<MultiGrid
cellRenderer={cellRend}
columnWidth={125}
columnCount={this.state.columnLen}
enableFixedColumnScroll ={1}
enableFixedRowScroll ={1}
fixedColumnCount
fixedRowCount
height={300}
rowHeight={70}
rowCount={2}
style={STYLE}
styleBottomLeftGrid={STYLE_BOTTOM_LEFT_GRID}
styleTopLeftGrid={STYLE_TOP_LEFT_GRID}
styleTopRightGrid={STYLE_TOP_RIGHT_GRID}
width={width}
hideTopRightGridScrollbar
hideBottomLeftGridScrollbar
hideBottomRightGridScrollbar
/>
)}
</AutoSizer>
</div>
)
}
}
export default connect(mapStateToProps, dispatchToProps)(stockPage);
From your title, I assume that when your page loads, you are fetching data then you use that data in the page. However, during initial load and when your fetching is still in process, your data is still null and your app will crash because the code is expecting data to have a value which it needs to use...
What you can do is while the data is fetching, then do not display the rest of the page yet (ie. you can just display a giant spinner gif), then once the fetching is complete then update isLoading state... Or you can set an initial value for the data so the page won't crash on load...
EDIT:
so using react lifecycle fixed your problem as per your comment... anyways just wanna add that you may want to use async/await instead of setTimeout like you did in your comment.. This is what the code might look like for async/await in lifecycles...
componentDidMount() {
const fetchData = async () {
await this.props.getSelStock() // your dispatch
// the state you want to update once the dispatch is done
this.setState({isLoading: false})
}
fetchData();
}

Type Error when i try to Render Objects out of an array

i get the following error when i try to load questions(Array) for a quiz from a db.json server.
TypeError: this.state.questions[this.state.currentQuestion] is undefined
here is a sandbox of my project (relevant classes are play_quiz.js and db.json
https://codesandbox.io/s/kjllt?file=/quiz-project/server/db.json
the error occurs in line 79 of play_quiz.js
it worked just fine when i did it without the json server but not it seems to have some kind of problem recognising the content of the array. I hope someone can help me
As discussed in the comments, the issue is that when the component renders for the first time, this.state.questions is an empty array as set in the state's initial state.
Because of this, this.state.questions[0] returns undefined, until the API response finishes (which is started by getRandomizedQuestions() which is called by componentDidMount after the component has been rendered and mounted).
You need to handle this case. The way to solve it depends on how you want to solve it and what you want to show to the user while it's loading. Here's a few solutions:
1. Just show "loading" until the array isn't empty.
render() {
if (this.state.questions.length < 1) {
return <div className="quiz-window">Loading...</div>;
}
return (
<div className="quiz-window">
// ...
</div>
);
}
2. Use another ternary to show "loading" in the place of the block of JSX which depends on the array:
render() {
return (
<div className="quiz-window">
{this.state.showScore ? (
<div className="score-section">
korrekt beantwortet: {this.state.score} von{" "}
{this.state.questions.length}
</div>
) : this.state.questions.length > 0 ? (
<>
<div className="question-section">
<div className="question-count">
<span>Frage {this.state.currentQuestion + 1}</span>/
{this.state.questions.length}
</div>
<div className="question-text">
{this.state.questions[this.state.currentQuestion].title}
</div>
</div>
<div className="answer-section">
{this.state.questions[this.state.currentQuestion].answers.map(
(answer) => (
<button
onClick={() => this.handleAnswerOptionClick(answer.isCorrect)}
>
{answer.title}
</button>
)
)}
</div>
</>
) : (
<p>Loading</p>
)}
</div>
);
}
Alternatively, if it's possible that the API returns an empty array and you want to handle that as a separate case:
3. Introduce state variable that tracks when the data is loading and show loading until it's done
class Play_quiz extends React.Component {
state = {
currentQuestion: 0,
showScore: false,
score: 0,
questions: [],
isLoading: true,
};
// ...
getRandomizedQuestions = () => {
const apiUrl = "http://localhost:3001/questions";
fetch(apiUrl)
.then((response) => response.json())
.then(
(result) => {
console.log("From database:");
console.log(result);
let amountOfQuestions = result.length;
let randomizedResult = [];
for (let i = 0; i < amountOfQuestions; i++) {
let randomIndex = Math.floor(Math.random() * result.length);
randomizedResult.push(result[randomIndex]);
result.splice(randomIndex, 1);
}
this.setState({
questions: randomizedResult,
isLoading: false
});
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
},
(error) => {
console.log("An unexpected error occurred", error);
}
);
};
// ...
render() {
if (this.state.isLoading) {
return <div className="quiz-window">Loading...</div>;
}
return (
<div className="quiz-window">
// ...
</div>
);
}
}
See also: Why calling react setState method doesn't mutate the state immediately?

How can I add delete buttons to clear whole state and remove specific state that .map function turns?

I tried to initial the array to remove all elements at inside of it but it did not work.
clearAll(){
this.setState({
posts: []
})
}
componentDidMount() {
axios.get('https://jsonplaceholder.typicode.com/posts')
.then(Response => {
console.log(Response)
this.setState({posts:Response.data})
})
.catch(error => {
console.log(error)
this.setState({errorMsg: "Error retreiving data !"})
})
}
render() {
const {posts , errorMsg} = this.state
return (
<div>
Post List Here.
{
posts.length ?
posts.map(post => <div> key={post.id}>{post.title}</div>) :
null
}
{ errorMsg ? <div>{errorMsg}</div> : null}
</div>
);
}
I checked shopping carts and watched videos about that to find button that removes specific state but it did not work again. Can someone give me a button or idea about that? I need 2 buttons for removing all state that are coming from .json and removing specific one of them.
call your clearAll function to delete all posts,
clearAll(){
this.setState({
posts: []
})
}
removePost(postId){
this.setState(state=>({posts: state.posts.filter(post=> post.id !== postId)})
}
to remove specific post. Call inside post <div/> button onClick
render() {
const {posts , errorMsg} = this.state
return (
<div>
Post List Here.
{
posts.length ?
posts.map(post => <div key={post.id}><div>{post.title}</div><button type="button" onClick={()=>this.removePost(post.id)}>Remove Post</button></div>) :
null
}
{ errorMsg ? <div>{errorMsg}</div> : null}
<button type="button" onClick={()=>this.clearAll()}>Remove All Posts</button>
</div>
);
}
create the
const initatialState={
posts:[],
errorMsg:''
}
when you want cleat ued
clearAll=()=>{
this.setState({...initatialState})}
tell me if it work in commit

React grid elements, show details about specified element after clicking it

i have created gird of elements (something like gallery) AllElements Component where i am mapping SingleElement Component
renderAllElements = () => (
this.state.myData.map(se => (
<SingleElement key={se.id} name={se.name} tagline={se.tagline} image_url={se.image_url}/>
)
)
)
And my SingleElement renders this, as below
render() {
return (
<div className="singleElement">
<img src={this.props.image_url} alt={this.props.name} />
<h4>{this.props.name}</h4>
<h5>{this.props.tagline}</h5>
</div>
)
}
To the point, what I want achieve? ---> After clicking on one of the elements (specyfied SingleElement) the details is shown in front of the screen (hovering over whole grid). Let's name this Component SingleElementDetails. What is the best way to achieve it? Should SingleElementDetails Component be sibling of SingleElement Component or it's child ?
You could use the AllElements state and an method to handle when/what to show.
Something like this:
class AllElements extends React.Component {
constructor(props) {
super(props);
this.state = {
myData: {},
viewingElement: null,
};
this.see = this.see.bind(this);
this.close = this.close.bind(this);
}
see(id) {
this.setState({
viewingElement: id,
});
}
close() {
this.setState({
viewingElement: null,
});
}
render() {
const { myData, viewingElement } = this.state;
return (
<div>
{myData.map(se => (
<SingleElement
key={se.id}
name={se.name}
tagline={se.tagline}
image_url={se.image_url}
see={this.see}
close={this.close}
/>
))}
{viewingElement && (
<SingleElementDetails element={myData[this.state]} />
)}
</div>
);
}
}
Then you need to fire this.props.see on the onClick event from SingleElement and use CSS to visually position SingleElementDetails over the rest of the contest.

Categories

Resources