🐥

ReactとFirebaseを使ってチャットアプリにアイコン画像を追加する方法

2021/03/31に公開

はじめに

名前とメッセージのみ表示されるチャットアプリに、アイコン画像を追加したいと思ったところ、意外と躓いたので私なりの方法を紹介します。
手順は以下の通りです。

  • <input type="file" />を使いフォームで画像を送信する
  • FirebaseのStorageで画像を参照、アップロードする
  • Storageからダウンロードし、Authenticationのユーザー情報を更新する
  • Firestoreのコレクションにユーザー情報の画像を追加する
  • Firestoreからデータを取得する
  • データを画面上にレンダリングする

前提:追加前のコード

アイコン画像追加前の処理は以下のようになっています。

SignUp.jsにてサインアップフォームにて入力したメールアドレスとパスワードを元に、

SignUp
firebase.auth().createUserWithEmailAndPassword(email, password)

Authenticationにユーザーを作成。
その処理が成功後、
user.updateProfile({displayName: name});
ユーザー名を更新し、history.push("/");でチャットルームへ遷移。

AuthService.jsでAuthenticationからユーザー情報を取得。

AuthService
useEffect(() => {
    firebase.auth().onAuthStateChanged(function (user) {
      setUser(user);
    });
  }, []);

Room.jsにて

Room
firebase.firestore().collection("messages").add({
      content: value,
      user: user.displayName,
      time: firebase.firestore.FieldValue.serverTimestamp(),
    });

Firestoreのメッセージコレションに、

  • フォームで入力したチャットメッセージの内容
  • ユーザー名
  • 投稿時間

を追加。

Room
firebase
 .firestore()
 .collection("messages")
 .orderBy("time")
 .onSnapshot((snapshot) => {
   const messages = snapshot.docs.map((doc) => {
     return doc.data();
   });
   setMessages(messages);

変数messageにfirestoreのメッセージコレクションの内容を配列として生成し、リアルタイムアップデート。
なおその際に、orderBy("time")を用いて、前述の変数timeの順番にソートしています。

map関数で<ul></ul>内にリストとして各コンポーネントにmessageを画面上にレンダリング。

以上です。
以下は全体のコードになります。

App
import React from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";

import { Login } from "./pages/Login";
import { SignUp } from "./pages/SignUp";
import { Room } from "./pages/Room";
import { AuthProvider } from "./AuthServise";
import { LoggedInRoute } from "./LoggedInRoute";

export const App = () => {
  return (
    <AuthProvider>
      <BrowserRouter>
        <Switch>
          <LoggedInRoute exact path="/" component={Room} />
          <Route exact path="/login" component={Login} />
          <Route exact path="/signup" component={SignUp} />
        </Switch>
      </BrowserRouter>
    </AuthProvider>
  );
};
SignUp
import React, { useState } from "react";
import firebase from "../config/firebase";

export const SignUp = ({ history }) => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [name, setName] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    firebase
      .auth()
      .createUserWithEmailAndPassword(email, password)
      .then(({ user }) => {
        user.updateProfile({
         displayName: name,
        });
        history.push("/");
      })
      .catch((err) => {
        console.log(err);
      });
  };
  return (
    <>
      <h1>Sign Up</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">E-mail</label>
          <input
            type="email"
            name="email"
            id="email"
            value={email}
            placeholder="Email"
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="password">Password</label>
          <input
            type="password"
            name="password"
            id="password"
            value={password}
            placeholder="password"
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="name">Name</label>
          <input
            type="name"
            name="name"
            id="name"
            value={name}
            placeholder="name"
            onChange={(e) => setName(e.target.value)}
          />
        </div>
        <button type="submit">Sign Up</button>
      </form>
    </>
  );
};
AuthService
import React, { useEffect, useState } from "react";
import firebase from "./config/firebase";

const AuthContext = React.createContext();

const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  useEffect(() => {
    firebase.auth().onAuthStateChanged(function (user) {
      setUser(user);
    });
  }, []);

  return <AuthContext.Provider value={user}>{children}</AuthContext.Provider>;
};

export { AuthContext, AuthProvider };
Room
import React, { useContext, useEffect, useState } from "react";
import { AuthContext } from "../AuthServise";
import firebase from "../config/firebase";
import { Item } from "./Item";

export const Room = () => {
  const [value, setValue] = useState("");
  const [messages, setMessages] = useState(null);

  useEffect(() => {
    firebase
      .firestore()
      .collection("messages")
      .orderBy("time")
      .onSnapshot((snapshot) => {
        const messages = snapshot.docs.map((doc) => {
          return doc.data();
        });
        setMessages(messages);
      });
  }, []);

  const user = useContext(AuthContext);

  const handleSubmit = (e) => {
    e.preventDefault();
    firebase.firestore().collection("messages").add({
      content: value,
      user: user.displayName,
      time: firebase.firestore.FieldValue.serverTimestamp(),
      avatar: user.photoURL,
    });
    setValue("");
  };

  return (
    <>
      <h1>Room</h1>
      <ul>
        {messages &&
          messages.map((message, index) => {
            return (
              <Item
                key={index}
                user={message.user}
                content={message.content}
              />
            );
          })}
      </ul>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={value}
          onChange={(e) => setValue(e.target.value)}
        />
        <button type="submit">送信</button>
      </form>
      <button onClick={() => firebase.auth().signOut()}>Logout</button>
    </>
  );
};

Item
export const Item = ({ user, content }) => {
  return (
    <li>
      {user} : {content}
    </li>
  );
};

チャットルームの表示は以下になっています。

アイコン画像追加処理

本題のアイコン画像追加の処理を行っていきます。

<input type="file" />を使いフォームで画像を送信する

SignUp.jsに画像送信用のinputを追加します。

SignUp
const [avatar, setAvatar] = useState(null);
...
<div>
  <label htmlFor="avatar">ユーザー画像</label>
  <input
    type="file"
    name="avatar"
    id="avatar"
    onChange={(e) => setAvatar(e.target.files[0])}
  />
</div>

Email,Password,Nameではvalue属性を用いてStateを更新していましたが、こちらはファイルへのパスを表す文字列となってしまうため、今回はfiles属性を使います。
files属性では選択されたfilelistが配列として入っています。
今回はファイルを1つだけ選択する仕様にしているため、インデックス番号[0]のファイルをavatarのState更新に使用しています。

FirebaseのStorageで画像を参照、アップロードする

Authenticationには画像を直接追加することは出来ないため、storageにアップロードし、そのURLをプロフィールに追加します。
まずstorageへのアップロードからです。

SignUp
const iconRef = firebase
  .storage()
  .ref()
  .child("user-image/" + avatar.name);
  
firebase
  .auth()
  .createUserWithEmailAndPassword(email, password)
  .then(({ user }) => {
    iconRef.put(avatar).then(() => {    
      iconRef.getDownloadURL().then((url) => {
        user.updateProfile({
          displayName: name,
	  photoURL: url,
        });
        history.push("/");
      });
    });
  })
  .catch((err) => {
    console.log(err);
  });

行った処理を説明します。

SignUp
firebase.storage().ref().child("user-image/" + avatar.name);

storageのuser-imageフォルダにavatar画像へのパスを参照します。
createUserWithEmailAndPassword(email, password)の処理が成功したら、putメソッドでstorageにアップロードします。

SignUp
.createUserWithEmailAndPassword(email, password)
  .then(({ user }) => {
    iconRef.put(avatar).then(() => { 

Storageからダウンロードし、Authenticationのユーザー情報を更新する

アップロードが成功したら、getDownloadURL()で、そのダウンロードURLを取得する処理を行います。

SignUp
iconRef.put(avatar).then(() => {    
  iconRef.getDownloadURL().then((url) => {

URLを取得したら、Authenticationのユーザープロフィール内photoURL(プロフィール写真のURL)に取得したURLを追加します。

SignUp
iconRef.getDownloadURL().then((url) => {
  user.updateProfile({
  displayName: name,
  photoURL: url,
});

これでユーザープロフィールに画像が更新されました。

Firestoreのコレクションにユーザー情報の画像を追加する

チャットルームで画像を表示するため、Firestoreのメッセージコレクションにプロフィール写真を追加します。
Room.jshandleSubmit内、メッセージコレクションへの追加要素にavatar: user.photoURLを加えます。
このuserAuthService.js内でAuthenticationから取得したユーザー情報です。

Room
const handleSubmit = (e) => {
  e.preventDefault();
  firebase.firestore().collection("messages").add({
    content: value,
    user: user.displayName,
    time: firebase.firestore.FieldValue.serverTimestamp(),
    avatar: user.photoURL,
  });
  setValue("");
};

Firestoreからデータを取得する

先程のメッセージコレクションからデータを取得します。
この処理に画像追加処理前との変更点はありません。

Room
firebase
 .firestore()
 .collection("messages")
 .orderBy("time")
 .onSnapshot((snapshot) => {
   const messages = snapshot.docs.map((doc) => {
     return doc.data();
   });
   setMessages(messages);

データを画面上にレンダリングする

Room.js<ul></ul>内でItemコンポーネントにメッセージコレクションのavatarをオブジェクトとして渡し、
Itemコンポーネントの<li></li>内に
<img style={{ width: "100px" }} src={avatar} alt="" />
を追加。
これでアイコン画像をチャットルームに表示することが出来ました。

変更後のコード

変更後はこのようなコードになります。

SignUp
import React, { useState } from "react";
import firebase from "../config/firebase";

export const SignUp = ({ history }) => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [name, setName] = useState("");
  const [avatar, setAvatar] = useState(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    const iconRef = firebase
      .storage()
      .ref()
      .child("user-image/" + avatar.name);
    firebase
      .auth()
      .createUserWithEmailAndPassword(email, password)
      .then(({ user }) => {
        iconRef.put(avatar).then(() => {
          iconRef.getDownloadURL().then((url) => {
            user.updateProfile({
              displayName: name,
              photoURL: url,
            });
            history.push("/");
          });
        });
      })
      .catch((err) => {
        console.log(err);
      });
  };
  return (
    <>
      <h1>Sign Up</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">E-mail</label>
          <input
            type="email"
            name="email"
            id="email"
            value={email}
            placeholder="Email"
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="password">Password</label>
          <input
            type="password"
            name="password"
            id="password"
            value={password}
            placeholder="password"
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="name">Name</label>
          <input
            type="name"
            name="name"
            id="name"
            value={name}
            placeholder="name"
            onChange={(e) => setName(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="avatar">ユーザー画像</label>
          <input
            type="file"
            name="avatar"
            id="avatar"
            onChange={(e) => setAvatar(e.target.files[0])}
          />
        </div>
        <button type="submit">Sign Up</button>
      </form>
    </>
  );
};
Room
import React, { useContext, useEffect, useState } from "react";
import { AuthContext } from "../AuthServise";
import firebase from "../config/firebase";
import { Item } from "./Item";

export const Room = () => {
  const [value, setValue] = useState("");
  const [messages, setMessages] = useState(null);

  useEffect(() => {
    firebase
      .firestore()
      .collection("messages")
      .orderBy("time")
      .onSnapshot((snapshot) => {
        const messages = snapshot.docs.map((doc) => {
          return doc.data();
        });
        setMessages(messages);
      });
  }, []);

  const user = useContext(AuthContext);

  const handleSubmit = (e) => {
    e.preventDefault();
    firebase.firestore().collection("messages").add({
      content: value,
      user: user.displayName,
      time: firebase.firestore.FieldValue.serverTimestamp(),
      avatar: user.photoURL,
    });
    setValue("");
  };
  
  return (
    <>
      <h1>Room</h1>
      <ul>
        {messages &&
          messages.map((message, index) => {
            return (
              <Item
                key={index}
                user={message.user}
                content={message.content}
                avatar={message.avatar}
              />
            );
          })}
      </ul>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={value}
          onChange={(e) => setValue(e.target.value)}
        />
        <button type="submit">送信</button>
      </form>
      <button onClick={() => firebase.auth().signOut()}>Logout</button>
    </>
  );
};
Item
export const Item = ({ user, content, avatar }) => {
  return (
    <li>
      <img style={{ width: "100px" }} src={avatar} alt="" />
      <br />
      {user} : {content}
    </li>
  );
};

Discussion