React のカスタムフックでロジックを共有する

React のカスタムフックを使って、複数のコンポーネントでロジックを共有する。

今まで作っていた物の仕様変更に伴い、複数のコンポーネントでロジックを共有するためにカスタムフックを使ってみた。

GitLab に完成品を置いた。

元の仕様

  • ページは「記事一覧」「ログイン」「記事投稿」の 3 画面
  • 「記事投稿」画面はログイン済の状態でしか表示できない

新仕様

  • ページは「記事一覧」「ログイン」の 2 画面
  • 「ログイン」画面は未ログイン状態でしか表示できない
  • 「記事一覧」画面をログイン済の状態で閲覧した場合、記事投稿フォームが画面上部に表示される

共有するロジックは

  • ユーザーが現在ログインしているかどうかの判定

で、これを下記の 2 か所で共有する。

  • 「ログイン」画面は未ログイン状態でしか表示できない
  • 「記事一覧」画面をログイン済の状態で閲覧した場合、記事投稿フォームが画面上部に表示される

カスタムフック作成

元々ログインしているかどうかの判定をどのように実装していたかは

Firebase Authentication で認証状態になったときだけ閲覧できるページを作る

という記事に書いた。

上記記事の認証状態判定を useAuth というカスタムフックに切り出す。

src/hooks/Auth/useAuth.js

import { useState, useEffect } from "react";
import { app } from '../../config/firebase';
import { getAuth, onAuthStateChanged } from "firebase/auth";

export const useAuth = () => {
    const [signinCheck, setSigninCheck] = useState({ signinCheck: false, signedIn: false });

    useEffect(() => {
        const auth = getAuth();
        var unsubscribe = onAuthStateChanged(auth, (user) => {
            if (user) {
                setSigninCheck({ signinCheck: true, signedIn: true });
            } else {
                setSigninCheck({ signinCheck: true, signedIn: false });
            }
        })
        unsubscribe();
    }, []);

    return [signinCheck];
};

『「ログイン」画面は未ログイン状態でしか表示できない』の実装

ログインしていたら「記事一覧」画面にリダイレクトし、ログインしていなかったら呼び出されたパスを表示するコンポーネントを作成する。

src/NotAuth.jsx

import React from 'react';
import { Redirect } from 'react-router-dom';
import { useAuth } from "./hooks/Auth/useAuth";

function NotAuth(props) {
    const [signinCheck] = useAuth();
    if (!signinCheck.signinCheck) {
        return (
            <div>Loading...</div>
        );
    }
    if (signinCheck.signedIn) {
        return <Redirect to="/" />
    } else {
        return props.children;
    }
}

export default NotAuth;

これを使ってルーティングを行う。NotAuth コンポーネントで囲んだ部分がログインしていない場合だけ表示するページになる。

src/App.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route
} from "react-router-dom";
import List from './List';
import Signin from './Signin';
import NotAuth from './NotAuth';
import Error from "./Error";

export default function App() {
  return (
    <Router>
      <Switch>
        <Route exact path="/" component={List} />
        <NotAuth>
          <Switch>
            <Route exact path="/signin" component={Signin} />
            <Route component={Error} />
          </Switch>
        </NotAuth>
      </Switch>
    </Router>
  );
}

『「記事一覧」画面をログイン済の状態で閲覧した場合、記事投稿フォームが画面上部に表示される』の実装

記事投稿フォームのコンポーネントで、ログイン済みの場合のみ記事投稿フォームを返すようにする。

src/Input.jsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React from "react";
import { useText } from './hooks/Input/useText';
import { useAuth } from "./hooks/Auth/useAuth";
import handleSignout from "./hooks/Input/handleSignout";
import Button from './component/Button';
import Submit from "./component/Submit";

function Form() {
  const [text, { handleChange, handleSubmit }] = useText();

  return (
    <form className="my-2 grid grid-cols-1 gap-y-2" id="input-form" onSubmit={handleSubmit}>
      <textarea className="dark:bg-gray-700 border border-gray-500 rounded p-0.5 text-gray-900 dark:text-white" id='text' name='text' value={text} onChange={handleChange} rows="5" cols="100" />
      <Submit value="送信" />
    </form>
  );
}

function Signout() {
  return (
    <Button onClick={handleSignout} name="ログアウト" />
  );
}

function Input() {
  const [signinCheck] = useAuth();

  if (signinCheck.signinCheck && signinCheck.signedIn) {
    return (
      <div>
        <Form />
        <Signout />
      </div>
    );
  }
  return null;
}

export default Input;

あとは記事一覧に上記コンポーネントを埋め込めば完成。

以上