Table of contents
Introduction
You might have seen on streaming platforms such as youtube.com or twitch.com where there are live chat feeds, whenever a new chat comes, mainly two things happen:
1. Auto-scrolls at the bottom
2. If a user scrolls the feed on the top then the auto-scroll behavior stops.
In this blog, we will try to build a react custom hook called useAutoScroll
which will help us to achieve the above behavior.
Let's start
So let's start with ChatMessagesList
component, which only knows how to render the given chats.
import React, { forwardRef } from "react";
interface ChatMessagesListProps {
chatMessages: {
message: string;
color: string;
username: string;
}[];
}
export const ChatMessagesList = forwardRef(
({ chatMessages }: ChatMessagesListProps, ref) => {
return (
<ul
ref={ref}
className="chat-list"
>
{chatMessages.length > 0 &&
chatMessages.map(({ color, username, message }, index) => (
<li key={index}>
<span style={{ color, fontWeight: "600" }}>{username}</span>:{" "}
{message}
</li>
))}
</ul>
);
}
);
You might have noticed forwardRef, is nothing but a way to pass the refs as a prop from the parent component to the current child component in this case from <App/>
to <ChatMessagesList/>
component.
Now let's see how the <App/>
component looks.
import { useEffect, useRef, useState } from "react";
import { ChatMessagesList } from "./ChatMessagesList";
import "./styles.css";
export default function App() {
const [chatMessages, setChatMessages] = useState<
{ color: string; message: string; username: string }[]
>([]);
useEffect(() => {
// fetch the chats and update the list
// using setChatMessages function
}, []);
const containerRef = useRef<HTMLElement | unknown>();
useEffect(() => {
const container = (containerRef?.current as HTMLElement) ?? {};
if (container) {
container.scrollTop = container.scrollHeight;
}
}, [chatMessages]);
return (
<div className="App">
<h1>Welcome to live chat</h1>
<ChatMessagesList ref={containerRef} chatMessages={chatMessages} />
</div>
);
}
The above solution is perfect for auto-scrolling the <ChatMessagesList />
component, but we wanted to build a reusable hook that can be used for any container.
So let's extract the reusable logic into our custom hook.
import { useEffect, useRef } from "react";
export const useAutoScroll = <T>(list: T) => {
const containerRef = useRef<HTMLElement | unknown>();
useEffect(() => {
const container = (containerRef?.current as HTMLElement) ?? {};
if (container) {
container.scrollTop = container.scrollHeight;
}
}, [list]);
return { containerRef };
};
So let's understand what's happening over here.
If you see carefully then useAutoScroll
is just a function that takes a list, whenever that list is updated, the useEffect is triggered, which just takes the HTML element's ref and updates its scrollTop value.
Let's also see what our <App />
component will look like now.
import { useEffect, useState } from "react";
import { ChatMessagesList } from "./ChatMessagesList";
import "./styles.css";
export default function App() {
const [chatMessages, setChatMessages] = useState<
{ color: string; message: string; username: string }[]
>([]);
useEffect(() => {
// fetch the chats and update the list
// using setChatMessages function
}, []);
// Note here
const { containerRef } = useAutoScroll(chatMessages);
return (
<div className="App">
<h1>Welcome to live chat</h1>
<ChatMessagesList ref={containerRef} chatMessages={chatMessages} />
</div>
);
}
There is one problem with the above hook, whenever a user scrolls on top, the container still scrolls back to the bottom.
To fix this let's update our useAutoScroll
hook.
import { useEffect, useRef } from "react";
export const useAutoScroll = <T>(list: T) => {
const containerRef = useRef<HTMLElement | unknown>();
const keepScrolling = useRef<boolean>(true); // note this
useEffect(() => {
const handleScroll = (e: Event) => {
const container = e.target as HTMLElement;
if (
container.scrollTop + container.clientHeight <
container.scrollHeight
) {
keepScrolling.current = false;
} else {
keepScrolling.current = true;
}
};
const container = (containerRef?.current as HTMLElement) ?? {};
if (container && keepScrolling.current) { // note this
container.scrollTop = container.scrollHeight;
}
container?.addEventListener("scroll", handleScroll); // note this
return () => container?.removeEventListener("scroll", handleScroll);
}, [list]);
return { containerRef };
};
So to stop the scroll whenever the user is scrolling on top we have kept one more ref which is keepScrolling
which tells the useEffect if it should update the scrollTop value or not.
Over here container.scrollTop
+ container.clientHeight
will equal to container.scrollHeight
which is the whole height of the overflowed list.
So whenever container.scrollTop
+ container.clientHeight
value becomes less thancontainer.scrollHeight
then it indicates that the user has scrolled the list, so we should stop auto-scrolling.
That's it, thanks for reading, I hope you learned something new, if yes then do share it with your friends.
You can find the whole code below:
https://codesandbox.io/s/useautoscroll-t26fcg?file=/src/hooks/useAutoScroll.ts
References
https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop
https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight
https://developer.mozilla.org/en-US/docs/Web/API/Element/clientHeight