ボクダイモリ

Life is like a Game

FirebaseとReact+Typescriptでチャットアプリを作ってみた

最近仕事でフロントエンドの開発をする機会があったので、勉強がてらチャットができるWebアプリを作ってみました。
実装の際にこだわった点としてクラスコンポーネントは使用せずに関数コンポーネントだけで実装することを徹底しました。
また、なるべくany型を使わずボタンのイベントを受け取る部分やライブラリが値を返す部分などもなるべく型を書くようにしてます。

github.com

Firebaseにデプロイしたのがこちら

https://react-chat-6e910.firebaseapp.com/

アプリのUIはスマホに対応していないのでスマホで見ると結構残念な感じになります。PCで見てください。 この記事では実装のポイントとかを書いていきます。

認証が必要なコンポーネントへのルーティング

アプリではログインせずにチャット画面に行こうとするとログイン画面にリダイレクトされます。 これを実現するため、ルーティングに使用するRouteコンポーネントにログインチェック処理をラップしたPrivateRouteコンポーネントを実装しました。 ルーティングはsrc/App.tsxに定義しています。

const App: React.FC = () => {
  return (
    <BrowserRouter>
      <Switch>
        <Route exact path="/" component={Login} />
        <Route path="/login" component={Login} />
        <PrivateRoute path="/chat/:roomID" component={Chat} />
        <Route exact path="/rooms" component={RoomList} />
        <Route path="/rooms/new" component={NewRoom} />
      </Switch>
    </BrowserRouter>
  );
}

PrivateRouteの実装は同じ階層のPrivateRoute.tsxにあります。 loginUserが存在すると受けとったコンポーネントに、存在しなければ/loginに遷移します。

const PrivateRouteComponent: FC<State> = ({
  loginUser,
  component: Component,
  ...rest
}) => {
  return (
    <Route
      {...rest}
      render={props =>
        loginUser !== null ? (
          <Route component={Component} {...props} />
        ) : (
          <Redirect to="/login" />
        )
      }
    />
  );
}

画面遷移を行わず動的に画面を変更する

画面遷移を行う方法としてaタグを使ったりformのPOST処理時に画面遷移をさせるということができますが、
アプリではいちいち画面遷移させたくなかったので、これらの方法は使用しませんでした。 代わりにreact-router-domパッケージにあるLinkタグが使えますが、これはJSXを書けるところでしか使えません。 そこで任意の箇所でHTMLを再構築できるようにreact-router-domのwithRouterを使います。 withRouterでラップされたコンポーネントはpropsからhistory APIにアクセスできるようになるので、これを操作して画面遷移を実現します。
例としてsrc/containers/newRoom.tsxではルーム作成ボタンを押された時に実行される関数内で、/rooms/newに遷移するようになっています。

const RoomListComponent = withRouter((props: RoomListProps) => {
  const { rooms } = props;
  const CreateNewRoom = (
    e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
  ) => {
    e.preventDefault();
    props.history.push(`/rooms/new`);
  };

レスポンシブ対応

CSSの設定にはstyled-componentsを使用していて、中でmediaクエリを設定しているだけです。
CSSは苦手というか見た目で頑張る気が起きなかったので一部の画面だけレスポンシブになってます。

const RoomList = styled.div`
  display: inline-flex;
  flex-wrap: wrap;
  width: 100%;
  padding-right: 1%;

  div {
    /* モバイルファースト */
    margin-top: 1%;
    margin-left: 1%;
    flex-basis: 100%;

    @media screen and (min-width: 768px) and (max-width: 1024px) {
      flex-basis: 49%;
    }

    @media screen and (min-width: 1024px) {
      flex-basis: 24%;
    }
  }
`;

チャット画面から抜ける際、メッセージのデータを初期化する

stateにあるメッセージを初期化する際、React HooksのuseEffectという機能を使っています。
下記はsrc/containers/Chat.tsxでの一例です。

const Chat = withRouter((props: ChatProps) => {
  const { match, initialDispatch } = props;
  useEffect(() => {
    return () => initialDispatch([]); // 以前訪れたチャットルームのメッセージが表示されないように初期化する
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

useEffect内ではコンポーネントのライフサイクルに応じて実行する処理を定義することができます。 上記ではinitialDispatch関数を実行する関数をreturnしていますが、これはコンポーネントがアンマウントされるタイミングで実行されます。 ReactのクラスコンポーネントでいうとcomponentWillUnmount()と同様の動きになります。
なぜこのようなことをしているかというとコメントに書いてある通りですが、以前訪れたチャットルームのメッセージをstateに残したままだと 別のチャットルームに入った時、一瞬だけ前に入ったチャットルームのメッセージが表示されてしまうためです。

止むを得ずany型を使用した箇所

ここで書くのは実装のポイントというか実装できなかった事です。 any型がある箇所は例えばsrc/containers/LoginOAuth.tsxです。

export interface DispatchProps {
  dispatchAuthStatus: (user: firebase.User | null) => void;
  handleGoogleLogin: () => void;
  handleTwitterLogin: () => void;
  handleGuestLogin: (e: any) => void; // なんとも忌々しいany型
}

any型のeではformからsubmitされた値が入ります。受け取るのはゲストログインのフォームからなのでユーザ名がsubmitのイベントに入ります。 handleGuestLoginの処理は下記のように定義しています。

  handleGuestLogin: e => {
    e.preventDefault();
    const loginName = e.currentTarget.children.loginName.value;
    const user: User = { displayName: loginName, photoURL: '' };
    dispatch(changeAuthStatus(user));
  },

フォームに入力されたユーザ名はe.currentTarget.children.loginName.valueで取得できます。 このsubmitされるloginNameをうまく型解決することができなかったためany型を使用しています。
そもそも入力フォームのname部分を型として制御できるのかとかその型を管理する意義があるかは置いといて、 うまく型解決する方法があるなら知りたいですね。

最後に

React書くの楽しいので今後も何か書くと思います。
CSSは疲れたのでもう頑張りたくないです。黙ってmaterial-uiとか使います。
あとフォームのバリデーションとかセキュリティ対策みたいなそういうことは何もしてないので、ちょっとドキドキしながらアプリを公開しています😌