Reputation: 11285
So I have a React application, and I want to scroll to the bottom of a div.
componentDidMount() {
this.pullMessages();
this.scrollToBottom();
}
pullMessages() {
var api = new API(this.props.keycloak);
var merchant_id = this.props.location.pathname.substr(this.props.location.pathname.lastIndexOf('/') + 1);
api.get("merchantMessages", { "repl_str": merchant_id }).then(
response => this.loadMessages(response.data)
).catch(function (error) {
console.log(error);
})
}
loadMessages(data) {
var count = 0;
var messagevals = [];
data.reverse().forEach(function (obj) {
messagevals.push(generateMessage(obj, count, {}));
count++;
});
this.setState({ messages: messagevals });
this.setState({ isLoading: false });
}
scrollToBottom = () => {
// Using this method because the reference didn't work
var bottomele = document.getElementById("bottom-scroll");
if (bottomele !== null) {
bottomele.scrollIntoView();
}
}
render() {
if (this.state.isLoading) {
return (<Loading />)
}
else {
return (
<div>
<div id="messages-container">
<div id="messages">
{ this.state.messages.map((message, index) =>
<div className={ "message " + message.position} key={index}>{message.text}</div>
}
<div className="bottom-scroll" id="bottom-scroll" ref={(el)=> { this.messagesEndRef = el; }}>
</div>
</div>
</div>
</div>
....
It is populated via an API call (and a loading modal is shown until this API call fills an array in state)
My problem is that I want to scroll to the bottom once the messages div is populated.
My issue is that my scroll to the bottom code seems to be executing before the messages are filled in, and so no scrolling happens.
How do I make sure to scroll only when my messages are populated and rendered? I've considered putting it in componentDidUpdate(), but the problem with that is I only want this scroll action to happen on first load, and then on message send.
Upvotes: 1
Views: 2369
Reputation: 158
You just need to use async/await to stop until data load. So you can update scroll when the data loading in your component as below:
pullMessages()
{
const { keycloak, location } = this.props;
const api = new API(this.props.keycloak);
const merchant_id = location.pathname.substr(location.pathname.lastIndexOf('/') + 1);
// this api.get function is async
api.get("merchantMessages", { "repl_str": merchant_id }).then((response) =>
{
this.loadMessages(response.data);
this.scrollToBottom(); // <--- this could work
}).catch(function (error)
{
console.log(error);
});
}
That previous example was first option to work normally. But you could do better as below:
componentDidMount()
{
this.pullMessages()
.then(() =>
{
this.scrollToBottom();
});
}
// The main thing is to use async operator
// to make function async and to wait also
// When you put the "async" keyword before the function
// the the function going to be async
async pullMessages()
{
const { keycloak, location } = this.props;
const api = new API(this.props.keycloak);
const merchant_id = location.pathname.substr(location.pathname.lastIndexOf('/') + 1);
// this api.get function is async
api.get("merchantMessages", { "repl_str": merchant_id }).then((response) =>
{
this.loadMessages(response.data);
}).catch(function (error)
{
console.log(error);
});
}
Actually the async/await came with ES7 and it uses Promise in background for you.
To MDN example:
var resolveAfter2Seconds = function()
{
console.log("starting slow promise");
return new Promise(resolve => {
setTimeout(function() {
resolve("slow");
console.log("slow promise is done");
}, 2000);
});
};
// and after that promise return async function, it will able to use as below
resolveAfter2Seconds()
.then((result) =>
{
console.log('process has been finished');
})
.catch(() =>
{
console.log(error);
});
// but we preferd to use that short one
// create async arrow function
const getData = async () =>
{
// When we use the await operator, it has to wait until process have finished
const getMyRemoteValue = await API.get(....);
// to use await keyword, the parent function must be async function
return getMyRemoteValue;
}
getData()
.then((yourRemoteData) =>
{
console.log('Your remote data is ready to use: ', yourRemoteData);
});
Also this answer might be helpful about scrolling around elements.
Upvotes: 0
Reputation: 30370
Consider making the following changes to your component:
React.createRef()
scrollToBottom()
after your data has loaded, to account for the asynchronous nature of the data request (ie, call scrollToBottom()
after you get a response from axios). In the case of your code, a good place to call scrollToBottom()
would be in loadMessages()
scrollToBottom()
in the callback of setState()
(ie when the messages
state data is updated)In code, these changes could be implemented as shown below:
constructor(props) {
super(props);
/*
Create ref to messages-container in constructor
*/
this.containerRef = React.createRef();
}
componentDidMount() {
this.pullMessages();
/* Remove this, we'll instead do this in loadMessages()
this.scrollToBottom();
*/
}
loadMessages(data) {
var count = 0;
var messagevals = [];
data.reverse().forEach(function (obj) {
messagevals.push(generateMessage(obj, count, {}));
count++;
});
/* After updating the message list with loaded data, cause the
messages-container to scroll to bottom. We do this via a call back
passed to setState() for this state update */
this.setState({ messages: messagevals, isLoading : false }, () => {
this.scrollToBottom()
});
}
scrollToBottom = () => {
const containerElement = this.containerRef.current;
if(containerElement) {
/* If container element exists, then scroll to bottom of
container */
containerElement.scrollTop = containerElement.scrollHeight;
}
}
render() {
if (this.state.isLoading) {
return (<Loading />)
}
else {
return (<div>
{/* Add ref here */ }
<div id="messages-container" ref={this.containerRef}>
<div id="messages">
{ this.state.messages.map((message, index) =>
<div className={ "message " + message.position}
key={index}>{message.text}</div>
}
{/*
This can go:
<div className="bottom-scroll"
id="bottom-scroll"
ref={(el)=> { this.messagesEndRef = el; }}>
</div>
*/}
</div>
</div>
</div>)
}
}
Hope that helps!
Upvotes: 1