環境
- React v16.9
- TypeScript v3.5.3
やりたかったこと
次のようなメッセージ画面に遷移した時、最新のメッセージの位置までスクロールした状態で表示したかった。
事前準備
- 親コンポーネント
ChatList
が子コンポーネントChatMessage
の配列を持つ。- ※簡略化のためクラス名などを省略している。
ChatList.tsx
import React from 'react';
import { ChatRoom } from '../../interfaces/chats';
import ChatMessage from './ChatMessage';
interface Props {
room: ChatRoom;
}
const ChatList: React.FC<Props> = ({ room }) => {
return (
<div>
<div>
{room.chats.map(chat => (
<ChatMessage key={chat.id} chat={chat} />
))}
</div>
</div>
);
};
export default ChatList;
ChatMessage.tsx
import React from 'react';
import { Chat } from '../../interfaces/chats';
interface Props {
chat: Chat;
}
const ChatMessage: React.FC<Props> = ({ chat }) => {
return (
<div>
{!chat.fromMe && (
<div>
<img src={chat.photo} alt="User" />
</div>
)}
<div>{chat.body}</div>
</div>
);
};
export default ChatMessage;
子コンポーネントから親コンポーネントに ref を渡す
- 子コンポーネントのPropsに
setRef
を追加する -
useRef
を用いて要素への参照を取得する -
useEffect
を用いてref
が更新されたタイミングでsetRef
を通して親コンポーネントにref
を渡す
ChatMessage.tsx
import React, { useEffect, useRef } from 'react';
import { Chat } from '../../interfaces/chats';
interface Props {
chat: Chat;
setRef: (chat: Chat, ref: React.RefObject<HTMLDivElement>) => void;
}
const ChatMessage: React.FC<Props> = ({ chat, setRef }) => {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
setRef(chat, ref);
}, [setRef, chat, ref]);
return (
<div ref={ref}>
{!chat.fromMe && (
<div>
<img src={chat.photo} alt="User" />
</div>
)}
<div>{chat.body}</div>
</div>
);
};
export default ChatMessage;
親コンポーネントで最後の要素の位置までスクロールする
- 子コンポーネントから ref を受け取ってスクロールするための関数
setLastMessageElement
を定義- 最後の要素だった場合、
scrollIntoView
を用いてその要素が見える位置までスクロールする
- 最後の要素だった場合、
-
setLastMessageElement
を子コンポーネントに props として渡す
ChatList.tsx
import React from 'react';
import { ChatRoom } from '../../interfaces/chats';
import ChatMessage from './ChatMessage';
interface Props {
room: ChatRoom;
}
const ChatList: React.FC<Props> = ({ room }) => {
const setLastMessageElement = (chat: Chat, ref: React.RefObject<HTMLDivElement>) => {
const lastChat = room.chats[room.chats.length - 1];
if (chat.id === lastChat.id) {
ref && ref.current && ref.current.scrollIntoView();
}
};
return (
<div>
<div>
{room.chats.map(chat => (
<ChatMessage key={chat.id} chat={chat} setRef={setLastMessageElement} />
))}
</div>
</div>
);
};
export default ChatList;
まとめ
- 後で気付いたんだけど、ユーザーが新しいメッセージを送った時に chats が更新されるから useEffect が実行されて、自動で最新のメッセージの位置までスクロールしてくれることが分かった。これは便利。