42
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

リアルタイムなLine風チャットアプリ(React + Firestore)

Last updated at Posted at 2021-08-01

githubソース firebase-realtime-chat1

はじめに

※ReactとFirebaseSDKの最新版に対応した記事を作りました

  • 画面イメージ

chat.png

  • Line風のチャットアプリです(複数人の投稿がリアルタイムに更新されます)
  • 利用のためにはFirestoreアカウントが必要です(googleのアカウントがあれば利用できます)
  • 初回利用時に名前を入力すると保存されます
  • バックエンドのロジックはありません(DBとして利用するFirestoreの同期機能を利用)
  • React + firebaseのSDKを利用します
  • アイコンは固定です(複雑化を避けるため)

利用モジュール

create-react-app + firebase のSDKのみです。

  "dependencies": {
    "@types/node": "^12.0.0",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "firebase": "^8.8.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-scripts": "4.0.3",
    "typescript": "^4.1.2"
  }

Firestoreについて

Firebaseとは、Google が提供しているモバイル、Webアプリ向けのバックエンドサービスです。Firestoreというリアルタイムで同期ができるNoSQLデータベースを利用します。

Firebaseプロジェクトの作成方法は下記の外部ページでご確認ください。

[Firebase] プロジェクトを新規に作成する

firebaseを操作するためのツールをインストールして、ログインします。(ブラウザが開き、Googleアカウントでログインを行います)

$ npm install -g firebase-tools

$ firebase login

Firestore接続設定

.envファイルで設定します。

設定する内容はfirebaseのコンソールから確認します。

firebase_config.png

  • .env
#Firebase configuration
REACT_APP_API_KEY=XXXXXXXXXXXXXXXXXXX
REACT_APP_AUTH_DOMAIN=myfirstproject-2b5c3.firebaseapp.com
REACT_APP_PROJECT_ID=myfirstproject-2b5c3
REACT_APP_STORAGE_BUCKET=myfirstproject-2b5c3.appspot.com
REACT_APP_MESSAGEING_SENDER_ID=XXXXXXXXXXXXXXXXXXX
REACT_APP_APP_ID=XXXXXXXXXXXXXXXXXXX
  • firebaseConfig.ts

Firebase SDKの初期化を行いdbをエクスポートします。

import firebase from 'firebase';

const firebaseConfig = {
  apiKey: process.env.REACT_APP_API_KEY,
  authDomain: process.env.REACT_APP_AUTH_DOMAIN,
  projectId: process.env.REACT_APP_PROJECT_ID,
  storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_MESSAGEING_SENDER_ID,
  appId: process.env.REACT_APP_APP_ID,
};

const firebaseApp = firebase.initializeApp(firebaseConfig);
const db = firebaseApp.firestore();
const auth = firebaseApp.auth();

export { auth };
export default db;

ソースコード

Line風の見た目はCSSだけでLINE風の「吹き出し」を作る方法を参考にさせていただきました。

html部分

チャットログはChatLog型の配列です。自分の名前のログを右側に、他人を左側に表示するようにclass名を切り替えています。

type ChatLog = {
  key: string,
  name: string,
  msg: string,
  date: Date,
};
  • Chat1.tsx
<>
  {/* チャットログ */}
  <div>      
    {chatLogs.map((item, i) => (
      <div className={userName===item.name? 'balloon_r': 'balloon_l'} key={item.key}>
        {userName===item.name? getStrTime(item.date): '' }
        <div className="faceicon">
          <img src={userName===item.name? './img/cat.png': './img/dog.png'} alt="" />
        </div>
        <div style={{marginLeft: '3px'}}>
          {item.name}<p className="says">{item.msg}</p>
        </div>
        {userName===item.name? '': getStrTime(item.date)}
      </div>
    ))}
  </div>
  
  {/* メッセージ入力 */}
  <form className='chatform' onSubmit={e => { submitMsg();e.preventDefault() }}>
    <div>{userName}</div>       
      <input type="text" value={msg} onChange={(e) => setMsg(e.target.value)} />
      <input type='image' onClick={submitMsg} src='./img/airplane.png' alt='' />       
  </form>
</>

データ同期処理

簡略化のため保存するテーブル名(collection)は固定です。

チャットルームのリファレンス(messagesRef)を取得して、最新10件を取得します。

onSnapshot()で変更イベントを受け取ります。別の人が追加したチャットメッセージを受け取り、ログに追加します。

  • 削除、更新イベントも発生しますが、動作に影響ないため追加のみを処理します。
  const [chatLogs, setChatLogs] = useState<ChatLog[]>([]);
  const messagesRef = useMemo(() => db.collection("chatroom").doc("room1").collection("messages"), []);

  useEffect( () => {
    // 同期処理イベント(最新10件をとるためdateでソート)
    messagesRef.orderBy("date", "desc").limit(10).onSnapshot((snapshot) => {
      snapshot.docChanges().forEach((change) => {
        if (change.type === "added") {
          // チャットログへ追加
          addLog(change.doc.id, change.doc.data());
          // 画面下部へスクロール
          window.scroll(0, document.documentElement.scrollHeight - document.documentElement.clientHeight)
        }
      });
    });
  },[]);

チャットメッセージ追加処理

  /**
   * チャットログに追加
   */
  function addLog(id: string, data: any) {
    const log = {
      key: id,
      ...data,
    };
    // Firestoreから取得したデータは時間降順のため、表示前に昇順に並び替える
    setChatLogs((prev) => [...prev, log,].sort((a,b) => a.date.valueOf() - b.date.valueOf()));
  }

ソースコード全体

import React, {useState, useMemo, useEffect} from 'react';
import db from './firebaseConfig';
import './chat1.css';

type ChatLog = {
  key: string,
  name: string,
  msg: string,
  date: Date,
};


/**
 * ユーザー名 (localStrageに保存)
 **/
const getUName = (): string =>  {
  const userName = localStorage.getItem('firebase-Chat1-username');
  if (!userName) {
    const inputName = window.prompt('ユーザー名を入力してください', '');
    if (inputName){
      localStorage.setItem('firebase-Chat1-username', inputName);
      return inputName;
    }    
  }
  return userName;
}

/**
 * UNIX TIME => hh:mm
 **/
function getStrTime(time: any){
  let t = new Date(time);
  return `${t.getHours()}`.padStart(2, '0') + ':' + `${t.getMinutes()}`.padStart(2, '0');
}


/**
 * チャットコンポーネント(Line風)
 */
const Chat1: React.FC = () => {
  const [chatLogs, setChatLogs] = useState<ChatLog[]>([]);
  const [msg, setMsg] = useState('');
  const userName = useMemo(() => getUName(), []);
  const messagesRef = useMemo(() => db.collection("chatroom").doc("room1").collection("messages"), []);

  useEffect( () => {
    // 同期処理イベント(最新10件をとるためdateでソート)
    messagesRef.orderBy("date", "desc").limit(10).onSnapshot((snapshot) => {
      snapshot.docChanges().forEach((change) => {
        if (change.type === "added") {
          // チャットログへ追加
          addLog(change.doc.id, change.doc.data());
          // 画面下部へスクロール
          window.scroll(0, document.documentElement.scrollHeight - document.documentElement.clientHeight)
        }
      });
    });
  // eslint-disable-next-line react-hooks/exhaustive-deps
  },[]);
  
  /**
   * チャットログに追加
   */
  function addLog(id: string, data: any) {
    const log = {
      key: id,
      ...data,
    };
    // Firestoreから取得したデータは時間降順のため、表示前に昇順に並び替える
    setChatLogs((prev) => [...prev, log,].sort((a,b) => a.date.valueOf() - b.date.valueOf()));
  }

  /**
   * メッセージ送信
   */
  const submitMsg = async () => {
    if (msg.length === 0) {
      return;
    }

    messagesRef.add({
      name: userName,
      msg: msg,
      date: new Date().getTime(),
    });

    setMsg("");
  };

  return (
    <>
      {/* チャットログ */}
      <div>      
        {chatLogs.map((item, i) => (
          <div className={userName===item.name? 'balloon_r': 'balloon_l'} key={item.key}>
            {userName===item.name? getStrTime(item.date): '' }
            <div className="faceicon">
              <img src={userName===item.name? './img/cat.png': './img/dog.png'} alt="" />
            </div>
            <div style={{marginLeft: '3px'}}>
              {item.name}<p className="says">{item.msg}</p>
            </div>
            {userName===item.name? '': getStrTime(item.date)}
          </div>
        ))}
      </div>
      
      {/* メッセージ入力 */}
      <form className='chatform' onSubmit={e => { submitMsg();e.preventDefault() }}>
        <div>{userName}</div>       
          <input type="text" value={msg} onChange={(e) => setMsg(e.target.value)} />
          <input type='image' onClick={submitMsg} src='./img/airplane.png' alt='' />       
      </form>
    </>
  );
};

export default Chat1;

参考ページ

[Firebase] プロジェクトを新規に作成する

[Firebase] Firestoreで読み書きする (Web編)

[Firebase] Firestoreでリアルタイムなチャットを作る (Web編) その1

CSSだけでLINE風の「吹き出し」を作る方法

42
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?