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>
Related
I am trying to render UI in my project based on selected roles (brands, agency, influencer) on click. However, the logic that I am putting together is not loading the right UI and I don't quiet understand why not.
I have tried moving the role and setRole to the top component and passed the props down to the child components that read role and updated it via setRole so that I can have the state to be available in two places.
I also set a logic that should display components based on if the role equals the value of the buttons.
What happened was the components weren't loading upon clicking the function that handles click. However, logging out to the console if the role equals the value of the clicked button returns true, the right string that the logic was based on.
What I am expecting to happen is to load the component e.g: "Brands" when users click and select "brands" which is the value of the clicked button. Vise versa for the other components.
My code is as follows:
import { useState } from 'react';
import { useSession } from 'next-auth/react';
import Brands from './Brands';
import Agency from './Agency';
import CreatorsDash from './CreatorsDashboard';
export default function FirstPageModal({ role: userRole }) {
const [role, setRole] = useState(userRole);
const { data: session } = useSession();
const handleClick = (e) => {
e.preventDefault();
let buttonValue = e.target.value;
const clickedRole = role?.map((user) => {
let { role } = user;
if (buttonValue) {
userRole = { role: buttonValue };
}
return { userRole };
});
console.log(clickedRole); //Returns an array
clickedRole.map((item) => {
const { role } = item.userRole;
console.log(role); //Returns string ("agency" / "brands" / "Influencer")
if (session && role === 'brands') {
console.log(role); //This logs "brands" as expected but doesn't return the component
// return <Brands session={session} role={role} />;
} else if (session && role === 'agency') {
return <Agency session={session} role={role} />;
} else if (session && role === 'Influencer') {
return <CreatorsDash session={session} role={role} />;
} else {
console.log('Select a role');
}
});
};
return (
<>
<div className="">
<button type="button" className="" onClick={handleClick} value="agency">
As an Agency
</button>
<button type="button" className="" onClick={handleClick} value="brands">
As a Brand
</button>
<button
type="button"
className=""
onClick={handleClick}
value="Influencer"
>
As an Influencer
</button>
</div>
</>
);
}
Returning a component from an onClick handler doesn't automatically render the component. One thing you could do is to keep track of the role in the state and then put the <Brands /> <Agency/> and <CreatorsDash /> components in the render function and dynamically show/hide them like {role === "brands" && <Brands />. This can also be done with css, although the benefits of this are not so clear,.
Side note, it is very helpful to post a codepen with your code, especially as your code gets more complicated
I am using react-transition-group to fade out various components. I'm converting simple conditional renders such as:
{valueToDisplay && <MyComponent {...valueToDisplay} />}
To transitions such as:
<CSSTransition
in={!!valueToDisplay}
unmountOnExit
classNames="fade"
addEndListener={(node, done) => node.addEventListener("transitionend", done, false)}
>
<MyComponent {...valueToDisplay} />
</CSSTransition>
The issue I'm running into is when the "in" property of the transition becomes false, and the exit transition is running, the child component will now have null prop values. This can cause exceptions or cause the child content to flash and change during the exit. What I would like to see instead is that during the exit transition, the content will remain unchanged.
The first solution I came up with was to make child components to cache previous values of their props, and then use those previous values when their props become null. However I don't like this solution because it forces all components which will be transitioned to introduce new and confusing internal logic.
The second attempt I made was to create a wrapper component which cached the previous value of props.children, and whenever "in" becomes false, renders the cached children instead. This essentially "freezes" the children as they were the last time in was true, and they don't change during the exit transition. (If this solution is the general practice, is there a better way of doing this, perhaps with the useMemo hook?)
For such a common use case of fading content out, this solution doesn't seem very intuitive. I can't help but feeling I'm going about this the wrong way. I can't really find any examples of having to cache/memoize content to keep it displaying during fade outs. It seems like something somewhere has to remember the values to display when performing the exit transition. What am I missing?
Here is a minimal example and working example:
import { useRef, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { CSSTransition } from 'react-transition-group';
const Pet = ({ type, age }) => {
return (
<div>
Your pet {type || 'null'} is age {age || 'null'}
</div>
);
};
const Fade = ({ show, children }) => {
const nodeRef = useRef(null);
return (
<CSSTransition
nodeRef={nodeRef}
in={show}
unmountOnExit
classNames="fade"
addEndListener={(done) => nodeRef.current.addEventListener("transitionend", done, false)}
>
<span ref={nodeRef}>
{children}
</span>
</CSSTransition>
);
};
const FadeWithMemo = ({ show, children }) => {
const previousChildren = useRef();
useEffect(() => {
previousChildren.current = show ? children : null;
}, [show, children]);
return (
<Fade show={show}>
{show ? children : previousChildren.current}
</Fade>
);
};
const Example = () => {
const [currentPet, setCurrentPet] = useState(null);
const getPet = () => {
return {
type: (Math.random() > .5) ? 'Cat' : 'Dog',
age: Math.floor(Math.random() * 15) + 1
};
};
return (
<>
<button onClick={() => setCurrentPet(getPet())}>Set</button>
<button onClick={() => setCurrentPet(null)}>Clear</button>
<div>
The Problem:
<Fade show={!!currentPet}>
<Pet {...currentPet} />
</Fade>
</div>
<div>
Potential Fix:
<FadeWithMemo show={!!currentPet}>
<Pet {...currentPet} />
</FadeWithMemo>
</div>
</>
);
};
const root = createRoot(document.getElementById('root'));
root.render(<Example />);
You can detach the visible condition from the pet state so that you have more granular control over whether something is visible and what is actually being displayed.
const Example = () => {
const [currentPet, setCurrentPet] = useState(null);
const [showPet, setShowPet] = useState(false);
const getPet = () => {
return {
type: (Math.random() > .5) ? 'Cat' : 'Dog',
age: Math.floor(Math.random() * 15) + 1
};
};
return (
<>
<button onClick={() => {
setCurrentPet(getPet());
setShowPet(true);
}}>Set</button>
<button onClick={() => setShowPet(false)}>Clear</button>
<div>
<Fade show={showPet}>
<Pet {...currentPet} />
</Fade>
</div>
</>
);
};
or you can have the visible be part of the pet state and only set that part to false.
I am trying to hide multiple divs with useSate.
They will be rendered random on the page, not from a list.
I have managed to do so by setting up different variables but couldn't find a more generic solution:
https://stackblitz.com/edit/react-t3shrc?file=src%2FApp.js
Also is there a way to close them when clicking outside?
Can you help please.
export default function App() {
const [isVisible, setIsVisible] = useState(false);
const [isVisible2, setIsVisible2] = useState(false);
const showInfo = (e, setIsVisible) => {
e.preventDefault();
setIsVisible(true);
};
const hideInfo = (e, setIsVisible) => {
e.preventDefault();
setIsVisible(false);
};
return (
<div>
<button
onClick={(e) => {
showInfo(e, setIsVisible);
}}
>
Show info 1
</button>
{isVisible && (
<div className="info">
Info 1
<button
onClick={(e) => {
hideInfo(e, setIsVisible);
}}
>
Close
</button>
</div>
)}
<br></br>
<br></br>
<button
onClick={(e) => {
showInfo(e, setIsVisible2);
}}
>
Show info 2
</button>
{isVisible2 && (
<div className="info">
Info 2
<button
onClick={(e) => {
hideInfo(e, setIsVisible2);
}}
>
Close
</button>
</div>
)}
</div>
);
}
I'm not 100% sure what you mean by a more 'generic' solution. Here is what comes to my mind:
First of all, we create a more complex object to basically hold all the variables / sections we encounter and use this as our state.
const initialVisibleAreas = {
area1: true,
area2: false
};
const [visibleAreas, setVisibleAreas] = useState(initialVisibleAreas);
Please note that this is propabably something you want to generate from your data using Object.keys(...) or mapping an array.
Next up, we create the functions for the buttons to use this new state accordingly:
// shows the element by given key
const showInfo = (event, key) => {
event.preventDefault();
setVisibleAreas({ ...visibleAreas, ...{ [key]: true } });
};
// hides the element by given key
const hideInfo = (event, key) => {
event.preventDefault();
setVisibleAreas({ ...visibleAreas, ...{ [key]: false } });
};
// sets every key to false to hide them all at once
const hideAllInfo = (event) => {
event.preventDefault();
const allFalse = Object.assign(
...Object.keys(visibleAreas).map((key) => ({ [key]: false }))
);
setVisibleAreas(allFalse);
};
Last but not least, we use them in jsx. This is basically one 'section':
<button
onClick={(e) => {
showInfo(e, 'area2');
}}
>
Show info 2
</button>
{
visibleAreas['area2'] && (
<div className="info">
Info 2
<button
onClick={(e) => {
hideInfo(e, 'area2');
}}
>
Close
</button>
</div>
);
}
To answer the last question; nothing is holding you to call 'hideAllInfo' inside a onClick handler of your surounding div. Sample is included in the modified stackblitz.
Have a complete look at the modified stackblitz
EDIT: In the case you want to close all areas by clicking the surrounding div, make sure to not propagate the button click event with:
event.stopPropagation();
I updated the stackblitz once again.
There is going to be an unusual amount of code here because I am trying to share everything that is going on.
What I am trying to do is... In a list:
Mark services as complete.
Change their color and hide after completion.
Show hidden services on a button press.
I managed to hide each individual service, but couldn't work with the button that hides/shows all of the completed services.
I have a context provider:
const ContextBooking = React.createContext()
const ContextProviderBooking = ({ children }) => {
const [isHidden, setIsHidden] = useState(false); //sharing among both components to hide/show list
return <ContextBooking.Provider value={{ isHidden, setIsHidden }}>
{children}
</ContextBooking.Provider>
}
export { ContextBooking, ContextProviderBooking }
Which is being passed over the BookingsDisplay component in another file
...
<ContextProviderBooking>
<BookingsDisplay /> //this encapsulates each <Booking />
</ContextProviderBooking>
...
I am rendering each of the services in a larger component called 'BookingsDisplay'
const BookingsDisplay = () => {
const { isHidden, setIsHidden } = useContext(ContextBooking)
const display = day => //function that displays each service according to the day it was booked for
allBookings.map( //allBookings is a json file
item =>
item.day === day && (
<Booking
isHidden={isHidden}
completed={item.completed} //comes from json file, all default to false
key={item.id}
id={item.id}
time={item.time}
name={item.name}
date={item.date}
/>
)
)
return (
<div className="bookings">
<h2 className="ib">Next bookings</h2>
<button //This won't work as expected and hide/show all of the 'completed' bookings
onClick={() =>{
setIsHidden(!isHidden);}
}>
Show hidden
</button>
<h2>Today</h2>
<ul> {display('today')} </ul>
<h2> Tomorrow </h2>
<ul> {display('tomorrow')} </ul>
<h2> General </h2>
<ul> {display('other')} </ul>
</div>
)
}
Each 'Booking' component has a button that marks the service as complete. This happens by conditionally changing the class of each component. This works fine as far as I'm concerned
const Booking = (props) => {
const [isHidden, setIsHidden] = useState(props.isHidden)
console.log(props.isHidden) // will output true or false 16 times(there are 16 component in total)
const [isCompleted, setIsCompleted] = useState(props.completed);
return (
<li
className={
isCompleted && isHidden ? 'booking-complete hide' //class names are not changing individually
: isCompleted ? 'booking-complete' //if button is pressed on one of them,
: 'booking' //it may affect the other
}
key={props.id}
id={props.id}>
<h3>{props.date}</h3>
<h4>{props.time}</h4>
<h5>{props.name}</h5>
<button
onClick={() => { //shouldn't this button work of each li and not sometimes all of them?
if (!isCompleted && !isHidden) {
setIsCompleted(!isCompleted); //this changes color of the service as className changes
setTimeout(() => setIsHidden(!isHidden), 200) //after a short time it is hidden
}
else if (isCompleted && !isHidden) {
setIsCompleted(!isCompleted);
}
else {
setIsCompleted(!isCompleted);
setIsHidden(!isHidden);
}
}}>
{!isCompleted ? `Completed` : `Not complete`}
</button>
</li>
)
}
There're two kind of isHidden in your app. I'mma call the one in context the global hidden isAllHidden and the one in <Booking /> the local hidden isHidden.
The problem is you misuse the two. local hidden is an internal state of <Booking />. The reason of it's existence is because you need that 200ms delay of animation, otherwise it can be replaced by isCompleted. So it should be derived from isCompleted instead of isAllHidden.
Fix 1:
const Booking = (props) => {
const [isHidden, setIsHidden] = useState(props.completed)
}
Now global hidden and local hidden combine to decide whether a Booking should hide. You logic should reflect this fact.
Fix 2:
const shouldBeHidden = Boolean(props.isAllHidden && isHidden)
return (
<li
className={
isCompleted && shouldBeHidden ? 'booking-complete hide'
: isCompleted ? 'booking-complete'on one of them,
: 'booking'other
}
>
...
Put together:
const Booking = (props) => {
const [isHidden, setIsHidden] = useState(props.completed)
const [isCompleted, setIsCompleted] = useState(props.completed)
const shouldBeHidden = props.isAllHidden && isHidden
return (
<li
className={
isCompleted && shouldBeHidden ? 'booking-complete hide' //class names are not changing individually
: isCompleted ? 'booking-complete' //if button is pressed on one of them,
: 'booking' //it may affect the other
}
>
<input type='checkbox' checked={isCompleted} onChange={() => {
setIsCompleted(!isCompleted)
setTimeout(() => setIsHidden(!isHidden), 200)
}}/>
<span>{props.name}</span>
</li>
)
}
I setup a demoboard here to show the result.
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.