【React】React Routerを使って、実質1行のコードでページ遷移とフラッシュメッセージ表示を行ってくれる機構を作ってみた

## はじめに
イベント処理後などにフラッシュメッセージの表示と異なるページへの画面遷移を行いたい場合があると思いますが、今回そのような機能を作成し、`呼び出す際に実質1行のコード記述で済む`ような機構を作ってみました！（※この機構自体が1行で済むわけではないです）

具体的には

* `ボタンクリック時（イベント発火時）にそれが誤った操作ならページ遷移と共にエラーメッセージを出力する`
* `ボタンクリック時（イベント発火時）にそれが正しい操作ならサクセスメッセージを出力させる`
* フラッシュメッセージを表示した後は非表示にする

ような機構です。

<br>

>_実装するのは以下のようなものです。_
![6pthr-6y5lz.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606750/25915af5-5852-77e0-518f-b427d4cb56f8.gif)

## 環境と使用パッケージ

* React（ver16.8以上）
 * React Hooksを使っての実装
 * カスタムフックを使っての実装
* React Router 
 * React Routerについては[React Router: Declarative Routing for React.js](https://reactrouter.com/web/guides/quick-start)を参照してください

## ファイル構造と作成するコンポーネント類



```
src
 ├─ App.js  以下のコンポーネントへのルーターとしての役割
 ├─ components
 │    ├─ components.css         以下のコンポーネント達のスタイリングを行う
 │    ├─ Home.js　　　　　        始点となるホームページ
 │    ├─ SuccessComponent.js    サクセスページ表示用のコンポーネント
 │    ├─ ErrorComponents.js     エラーページ表示用のコンポーネント
 │    ├─ FlashMessages.js       フラッシュメッセージ表示用のコンポーネント
 │
 ├─ hooks
      ├─ useFlashMessages.js    フラッシュメッセージとページ遷移を操作するカスタムフック
```

<br>

## 実装に入ります（下準備）

#### 1. まずはアプリ生成からです
```
$ create-react-app flash-messages-app
```

#### 2. React Routerをインストールです
```
$ yarn add react-router-dom
```
#### 3. ファイル達の作成です

以下のようにファイルを準備しておきます。

```
src
 ├─ App.js  以下のコンポーネントへのルーターとしての役割
 ├─ components
 │    ├─ Home.js　　　　　        始点となるホームページ
 │    ├─ SuccessComponent.js    サクセスページ表示用のコンポーネント
 │    ├─ ErrorComponents.js     エラーページ表示用のコンポーネント
 │    ├─ FlashMessages.js       フラッシュメッセージ表示用のコンポーネント
 │
 ├─ hooks
      ├─ useFlashMessages.js    フラッシュメッセージとページ遷移を操作するカスタムフック
```


#### 4. App.jsを開いてルーティングを設定です
```react:src/App.js
import React from 'react';
import './App.css';

// React RouterからはBrowserRouterとRouteコンポーネントをimportします
import {
  BrowserRouter as Router,
  Route,
} from "react-router-dom";

import Home from './components/Home';
import FlashMessages from './components/FlashMessages';
import SuccessComponent from './components/SuccessComponent';
import ErrorsComponent from './components/ErrorsComponent';

function App() {

  return (
    <div className="App">
      <Router>
          <FlashMessages />
          {/* ルートのURLをルーティングに指定 */}
          <Route exact path="/"
            render={props => (
              <Home
                history={props.history}
              />
            )}
          />
          {/* "/success"をルーティングに指定 */}
          <Route exact path="/success">
            <SuccessComponent />
          </Route>
          {/* "/errors"をルーティングに指定 */}
          <Route exact path="/errors">
            <ErrorsComponent />
          </Route>
      </Router>
    </div>
  );
}

export default App;
```

> * Homeコンポーネントではprops.historyを使ってページ遷移の操作を行うので、レンダープロップを使い、Homeコンポーネントにpropsを流しておきます。


#### 5. Home.jsコンポーネントを作成です
```react:src/components/Home.js
import React from 'react'

function Home({ history }) {

    const handleClick = e => {
        switch (e.target.value) {
            case "success":
                // サクセスコンポーネントへの遷移とフラッシュメッセージの表示処理
            case "errors":
                // エラーコンポーネントへの遷移とフラッシュメッセージの表示処理
            default:
        }
    }

    return (
        <div className="home">
            <h1>ホーム</h1>
            <button type="submit" onClick={handleClick} value="success">
                サクセスボタン
            </button>
            <button type="submit" onClick={handleClick} value="errors">
                エラーボタン
            </button>
        </div>
    )
}

export default Home
```

####上記コードの説明

>```react
function Home({ history }) { 
　
...
　
}
export default Home
```

> * 以下ではApp.jsで流したhistoryを受け取っています。

<br>

>```react
　
  ...
　
  const handleClick = e => {
        switch (e.target.value) {
            case "success":
                // サクセスコンポーネントへの遷移とフラッシュメッセージの表示処理
            case "errors":
                // エラーコンポーネントへの遷移とフラッシュメッセージの表示処理
            default:
        }
  }
　
  ...
　
  <button type="submit" onClick={handleClick} value="success">
    サクセスボタン
  </button>
  <button type="submit" onClick={handleClick} value="errors">
    エラーボタン
  </button>
　
  ...
```

> * handleClick()イベント：ボタンのvalueをトリガーとして、successならサクセスの処理を、errorならエラーの処理を実行させるようにします。
 * __ここにコードを1行記述することで、フラッシュメッセージの表示と画面遷移を実行させます。__
>* ボタンUIの実装：onClickにhandleClickイベントを渡しておき、valueにそれぞれのトリガーとなる単語を代入しておきます。

#### 6. SuccessComponent.jsコンポーネントを作成です
```react:src/components/SuccessComponent.js
import React from 'react'

// React RouterからLinkコンポーネントをimportする
import { Link } from 'react-router-dom'

function SuccessComponent() {
    return (
        <div className="success-component">
            <h1>サクセスページ</h1>
            <Link to="/">
                ホームへ
            </Link>
        </div>
    )
}

export default SuccessComponent
```

#### 7. ErrorsComponent.jsを作成です（ほとんど上のSuccessComponent.jsと同じです）
```react:src/components/ErrorsComponent.js
import React from 'react'
import { Link } from 'react-router-dom'

function ErrorsComponent() {
    return (
        <div className="errors-component">
            <h1>エラーページ</h1>
            <Link to="/">
                ホームへ
            </Link>
        </div>
    )
}

export default ErrorsComponent
```

#### 8.FlashMessages.jsコンポーネントを作成です
```react:src/components/FlashMessages.js
import React from 'react'

function FlashMessages() {

    return (
        <React.Fragment>
            <p className="errors"> {/* エラーメッセージが入ります！ */} </p>
            <p className="success"> {/* サクセスメッセージが入ります！ */} </p>
        </React.Fragment>
    )
}

export default FlashMessages
```

<br>
__ここまでで下準備が完了しました！
今からuseFlashMessages.jsカスタムフックを作成して、イベント発火時にフラッシュメッセージを表示させ、ページ遷移を行わせる機能を実装していきます！__
<br>

## 実装（フラッシュメッセージ表示&画面遷移機能）

#### 1. useFlashMessages.jsを作成します
```react:src/hooks/useFlashMessages.js
import { useState } from 'react'

function useFlashMessages() {
    const [flashMessages, setFlashMessages] = useState({
        success: "",
        errors: "",
    })

    const handleFlashMessages = ({ success, errors, history, pathname }) => {
        history.push(pathname)
        setFlashMessages({
            success: success ? success : "",
            errors: errors ? errors : "",
        })
        setTimeout(() => setFlashMessages({ success: "", errors: ""}), 2000)
    }

    return { flashMessages, handleFlashMessages }
}

export default useFlashMessages
```

__上記コードの説明__

> ```react
    const [flashMessages, setFlashMessages] = useState({
        success: "",
        errors: "",
    })
```

> * ここではエラーメッセージ、サクセスメッセージ用のステートを定義しています。ご覧のように1つのステートで管理するようにします。

<br>

> ```react
    const handleFlashMessages = ({ success, errors, history, pathname }) => {
        history.push(pathname)
        setFlashMessages({
            success: success ? success : "",
            errors: errors ? errors : "",
        })
        setTimeout(() => setFlashMessages({ success: "", errors: ""}), 2000)
    }
```

> * 以下ではhandleFlashMessages()関数を定義しています。
 * 引数ではそれぞれ次のような値が渡されることを宣言します。<br>
　　success : 表示させたいサクセスメッセージ<br>
　　errors : 表示させたいエラーメッセージ<br>
　　history : propsに含まれるhistory<br>
　　pathname : パス<br>
 * history.push(pathname) : ページ遷移させるための記述。
 * setFlashMessages() : ステートにメッセージを格納（undefinedにならないよう値が存在するかどうかのチェックも行う）
 * setTimeout() : 最後にステートを空にすることで、フラッシュメッセージを消す

<br>

> ```react
return { flashMessages, handleFlashMessages }
```

> * 最後にflashMessagesステートの変数と、handleFlashMessages()関数をreturnさせます。

<br>

#### 2. useFlashMessages.jsを使って、フラッシュメッセージの表示と画面遷移を操作していきます

#####　2-1. App.jsにて、useFlashMessages.jsのimportと、FlashMessagesコンポーネントへのflashMessages変数流し & HomeコンポーネントへのhandleFlashMessages()関数流し
```react:src/App.js
import React from 'react';
import './App.css';

import {
  BrowserRouter as Router,
  Route,
} from "react-router-dom";

import Home from './components/Home';
import FlashMessages from './components/FlashMessages';
import SuccessComponent from './components/SuccessComponent';
import ErrorsComponent from './components/ErrorsComponent';

// impotします！
import useFlashMessages from './Hooks/useFlashMessages';

function App() {
  const {flashMessages, handleFlashMessages} = useFlashMessages()

  return (
    <div className="App">
      <Router>
        {flashMessages &&
          <FlashMessages flashMessages={flashMessages} />
        }
        <Route exact path="/"
            render={props => (
              <Home
                handleFlashMessages={handleFlashMessages}
                history={props.history}
              />
            )}
          />
        <Route exact path="/success">
            <SuccessComponent />
        </Route>
        <Route exact path="/errors">
            <ErrorsComponent />
        </Route>
      </Router>
    </div>
  );
}

export default App;
```

__上記コードの説明__

> ```react
  const {flashMessages, handleFlashMessages} = useFlashMessages()
```

> * ここでは、useFlashMessages()カスタムフックから、flashMessages変数とhandleFlashMessages()関数を呼び出しています。

<br>

>```react
　
  ...
　
  {flashMessages &&
    <FlashMessages flashMessages={flashMessages} />
  }
    <Route exact path="/"
      render={props => (
        <Home  history={props.history}  handleFlashMessages={handleFlashMessages} />
      )}
    />
　
  ...
　
```

> * ここでは、FlashMessagesコンポーネントにflashMessages変数を流しかつ、flashMessagesが存在する場合にのみ表示させるようにします。
* HomeコンポーネントにはhandleFlashMessages()関数を流しています。

<br>

#####　2-2. FlashMessages.jsコンポーネントにて、flashMessages変数を使ってフラッシュメッセージを表示させます
```react:/src/components/FlashMessages.js
import React from 'react'

// App.jsで流したflashMessagesを受け取る
function FlashMessages({ flashMessages }) {

    return (
        <React.Fragment>
            {/* flashMessages変数の中身がerrorsならエラーメッセージを表示させ、中身がsuccessならサクセスメッセージを表示させる */}
            {flashMessages.errors && <p className="error">{flashMessages.errors}</p>}
            {flashMessages.success && <p className="success">{flashMessages.success}</p>}
        </React.Fragment>
    )
}

export default FlashMessages
```

<br>

#####　2-3.Home.jsコンポーネントにて、handleFlashMessages()関数を使い、イベント発火時にフラッシュメッセージが表示されるようにする。
```react:src/components/Home.js
import React from 'react'

function Home({ handleFlashMessages, history }) {

    const handleClick = e => {
        switch (e.target.value) {
            case "success":
                handleFlashMessages({
                    success: "サクセスボタンがクリックされました。",
                    history: history,
                    pathname: "/success"
                })
                break;
            case "errors":
                handleFlashMessages({
                    errors: "エラーボタンがクリックされました。",
                    history: history,
                    pathname: "/errors"
                })
                break;
            default:
        }
    }

    return (
        <div className="home">
            <h1>ホーム</h1>
            <button
                type="submit"
                onClick={handleClick}
                value="success"
            >
                サクセスボタン
            </button>
            <button
                type="submit"
                onClick={handleClick}
                value="errors"
            >
                エラーボタン
            </button>
        </div>
    )
}

export default Home
```

__上記のコード説明__

>```react
function Home({ handleFlashMessages, history }) {
　
  ...
　
}
```

> * App.jsで流したhandleFlashMessages()関数を受け取る

<br>

>```react
    const handleClick = e => {
        switch (e.target.value) {
            case "success":
                handleFlashMessages({
                    success: "サクセスボタンがクリックされました。",
                    history: history,
                    pathname: "/success"
                })
                break;
            case "errors":
                handleFlashMessages({
                    errors: "エラーボタンがクリックされました。",
                    history: history,
                    pathname: "/errors"
                })
                break;
            default:
        }
    }
```

> * handleFlashMessages()関数を使い、
 * success/errors : イベント発火時に表示させたいフラッシュメッセージを渡す
 * history : App.jsから流れてくるhistoryを渡す
 * pathname : 遷移させたいページ先のパスを渡す<br>

> * これにより、エラーボタンをクリックすれば`「エラーフラッシュ表示、エラーページへの遷移」`が行われ、サクセスボタンをクリックすれば`「サクセスフラッシュ表示、サクセスページへの遷移」`が行われるようになりました。

> * __後はこの関数を呼び出したいという箇所で、上階層からこのhandleFlashMessages()と、historyを流して、上記のように引数に適当な値を渡せば、フラッシュメッセージの表示とページ遷移が行われます。__

<br>

※ 注意点
--

Home.jsコンポーネントに直接useFlashMessages.jsをimportした方が早いだろうという考えから、そのようにしてhandleFlashMessages()関数を使用しようとすると、handleFlashMessages()関数内のステート変更処理時に、`アンマウントされたステートを変更しようとしているぞというWarning`が出て怒られるので、親コンポーネントから流して使うようにしなければなりませんので注意してください。
そもそも、`他の階層のステートを変更する処理を行う場合はそうする必要があります。`

<br>


※ 補足
--

* ここまで実装してきた内容はあくまで簡単な例でしたが、それじゃあ例えば実際にバックエンドとの通信を行い、レスポンスを受け取った際にフラッシュメッセージの表示とページ遷移を行わせたい場合はどのようにすれば良いのかを見ていきたいと思います。



* ここでも簡単な例となってしまいますが、例えば認証系の機能が実装されているとして、新規登録ボタンをクリックした場合、もしくはログインボタンをクリックした場合にフラッシュメッセージを表示させ、ページ遷移を行わせる方法を見てみます。

```react
// 非同期通信で行うとする
import axios from 'axios'

// ログイン機構用のカスタムフック
// 親階層からhistoryとhandleFlashMessages()関数を流してから、受け取る
// ログインイベントは新規登録とログインを扱っているので、methodにはpostが渡されてくると想定
// urlには新規登録用のバックエンドルーティングもしくは、ログイン用のバックエンドルーティングの２通りの値が渡ってくることになることを想定
function useLogin({ method, url, history, handleFlashMessages }) {

    // ログインイベント
    const handleSubmit = e => {
        e.preventDefault()
        axios[method](url,
            {
                user: {
                    email: "sample@example.com",
                    password: asdfasdf
                }
            })
            .then(response => {
                // 新規登録が成功した場合
                if (新規登録成功) {
                    // handleFlashMessages()を呼び出し、
                    // successに表示させたいサクセスメッセージ
                    // historyにはhistory
                    // pathnameには、ユーザーページへのパスを指定
                      // response.data.user_idにはバックエンド側からユーザーのIDを受け取ることを想定してこのように記述しています。
                    handleFlashMessages({
                        success: "正常に登録が完了しました",
                        history: history,
                        pathname: `/users/${response.data.user_id}`
                    })
                }
                // ログインに成功した場合
                if (ログイン成功) {
                    // 新規登録側と同じ
                    handleFlashMessages({
                        success: "正常にログインが完了しました。",
                        history: history,
                        pathname: `/users/${response.data.user_id}`
                    })
                }
            })
            .catch(err => console.log(err))
    }

    return {handleSubmit}
}

export default useLogin
```

* エラーレスポンスを受け取った場合も同じように記述するだけです


<br>

#####　2-4. 最後に見易くなるようUIを（申し訳程度に）整える
```react:src/App.css
* {
    margin: 0;
    padding: 0;
}

.home {
    background: rgba(128, 128, 128, 0.5);
    height: 100vh;
    width: 100vw;
}
.home > button[type="submit"] {
    position: relative;
    top: 50%;
}

.errors-component {
    background: rgba(255, 0, 0, 0.5);
    height: 100vh;
    width: 100vw;
}

.error {
    padding: 50px 0;
    color:red;
    font-weight: 600;
    background: white;
}

.success-component {
    background: rgba(0, 128, 0, 0.5);
    height: 100vh;
    width: 100vw;
}
.success {
    padding: 50px 0;
    color: green;
    font-weight: 600;
    background: white;
}

```

<br>

以上で実装自体は終了です、以下のように動作すれば完了です！

![6pthr-6y5lz.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606750/b4985e9b-203e-c080-5095-5fa862f50079.gif)



<br>

以上です！
最後まで読んで頂きありがとうございました。不足な点など御座いましたら仰って頂けると幸いです🙇‍♂️