create-react-app のメニュー表示と認証・入力内容での表示制御

## 概要
[create-react-app](https://github.com/facebook/create-react-app) を利用した基本的なSPAの実装サンプルの共有です。
メニュー表示と認証、イベント処理などを網羅的に扱っているサンプルがなかったので後世のために記事にしてみます。アプリケーションの形態としては管理機能が近いかと。

### アプリケーション外観
![list.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106217/f92a0f16-23f8-3f2c-1d59-da32007723a4.png)


ソースコードはGitHubの [react-trial](https://github.com/cobot00/react-trial) にて公開しているので参考にしてください。
なお、ReactやMateril-UIの扱いに至らぬところがあれば指摘をいただけるとありがたいです。

### この記事で触れる事項
* 認証の前後でのメニュー表示制御、リダイレクト制御
* セッション管理
* 入力内容に応じて関連する項目の表示を変更する
* ヘッダー固定のテーブルの実現方法
* Materil-UIの基本的な使い方

### この記事で触れない事項
Reactに固有の話ではない一般的な事項なのでこの記事では触れません。

* 認証処理
* APIリクエストの実行とレスポンスの処理

### 前提条件
* Reactのバージョン **16.8.6**
* 認証状態、セッション管理に **Context API** と [localStorage](https://developer.mozilla.org/ja/docs/Web/API/Window/localStorage) を利用しています
* ルーティングは **react-router 5.0.0** を利用しています
* ES6以降の記法でコードは記述しています

## 認証の前後でのメニュー表示制御、リダイレクト制御
### 認証前の表示
![認証前.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106217/97ebd70a-62c8-8942-483a-eb9fb29e13fd.png)

### 認証後の表示
![butons.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106217/af3d7f97-3958-0da5-5b34-6db5204e6305.png)

### 解説
認証されているかどうかで表示するメニューとルーティングを以下のようなコードで切り替えています。

```js
const Routing = (props) => {
  return (
    <BrowserRouter>
      <div>
        <ul className="flex-container" style={{paddingLeft: '5px'}}>
          <li>
            <div className="side-menu">
              { props.authenticated ? <AuthenticatedMenus /> : <NotAuthenticatedMenus /> }
            </div>
          </li>

          <li className="right-justified-item" style={{width: '100%'}}>
            { props.authenticated ? <AuthenticatedRouting /> : <NotAuthenticatedRouting /> }
          </li>
        </ul>
      </div>
    </BrowserRouter>
  );
};
```

[メニュー表示とルーティングの切替](https://github.com/cobot00/react-trial/blob/review/src/App.js#L58-L76)

認証前は以下のようなルーティングで `/login` 以外に遷移しようとすると `/login` にリダイレクトします。

```js
const NotAuthenticatedRouting = () => {
  return (
    <Switch>
      <Route path="/login" component={LoginForm} />
      <Route path='*'
        render={() => (
          <Redirect to="/login" />
        )}
      />
    </Switch>
  );
};
```

認証後は以下のルーティングが適用され、表示されるメニューに対応する画面が表示されるようになります。

```js
const AuthenticatedRouting = () => {
  return (
    <Switch>
      <Route path="/" exact component={HomeForm} />
      <Route path="/button" exact component={ButtonForm} />
      <Route path="/list" exact component={ListForm} />
      <Route path="/event" exact component={EventForm} />
      <Route path='*'
        render={() => (
          <Redirect to="/" />
        )}
      />
    </Switch>
  );
};
```

利用ユーザーの役割、権限などによって利用できるメニューが異なるようなケースではこの実装ではつらくなるので `Route` コンポーネントを継承・拡張してその中で利用可能なメニューかを判定するような設計、実装にした方がよいかと。

## 認証・セッション管理
`Context API` と独自のグローバルなセッション管理オブジェクトを組み合わせて認証状態・セッション管理を行っています。

```js
export const SessionContext = React.createContext({
  authenticated: false,
  login: () => {},
  logout: () => {}
});
```
```js
class SessionState {
  constructor() {
    this.session = {};
    this.authenticated = false;

    this.sessionKey = process.env.REACT_APP_SESSION_KEY || '';
    const session = JSON.parse(localStorage.getItem(this.sessionKey));
    if (session) {
      this.session = session;
      this.authenticated = true;
    }
  }

  login() {
    this.session = {
      // 中略
    };
    localStorage.setItem(this.sessionKey, JSON.stringify(this.session));
    this.authenticated = true;
  }

  logout() {
    localStorage.removeItem(this.sessionKey);
    this.session = {};
    this.authenticated = false;
  }
}

export const sessionState = new SessionState();
```

React実装のメニュー制御、表示制御は `Context API` で足りますが、WEBブラウザを終了させたり、リロードした場合にはセッション状態は消失して再度の認証が必要になってしまいます。
メモリ外にセッション状態を保持するために `localStorage` を利用してJavaScriptがロードされた際に以前のセッションを再現させるようにしています。
`SessionState` のJavaScriptがロードされると `new SessionState()` が実行され、インスタンス生成時に `localStorage` に保存されたセッションを確認し、存在する場合はメモリ上に展開します。

`App` コンポーネントが生成される際に `SessionState` の認証状態が反映され、以降のメニュー表示やルーティングの制御に伝播されていきます。

```js
this.state = {
  authenticated: sessionState.authenticated
};
```

[localStorageのセッションをReactの実装に反映する](https://github.com/cobot00/react-trial/blob/review/src/App.js#L28-L47)

## 入力内容に応じて関連する項目の表示を変更する
セレクトボックスの内容に応じてボタンの表示を変更する処理とチェックボックスのチェックの有無でテキスト入力欄の表示・非表示を切り替える処理の紹介です。
[実装の全容はこちら](https://github.com/cobot00/react-trial/blob/review/src/components/forms/EventForm.js)

### 初期状態
![イベント_初期状態.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106217/256b6d45-1a83-48c4-59b5-5c0a24aef496.png)

### 変更後
![イベント_変更状態.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106217/f989312f-62ac-c0d7-b013-0d128c21566b.png)

### セレクトボックスの選択内容でボタンの外観を変更する
Material-UI の [Select](https://material-ui.com/components/selects/) コンポーネントを利用しています。
セレクトボックスの `onChange` イベントに親コンポーネントの `onChangeSelect` イベントをバインドして親コンポーネントの `state` を変更するようにしています。

```js
<Select
  value={this.state.buttonColor}
  onChange={(e) => this.onChangeSelect(e, 'buttonColor')}
  style={{minWidth: '150px'}}
>
  <MenuItem value={'default'}>default</MenuItem>
  <MenuItem value={'primary'}>primary</MenuItem>
  <MenuItem value={'secondary'}>secondary</MenuItem>
</Select>
```

```js
onChangeSelect(e, key) {
  this.setState({[key]: e.target.value});
}
```

Material-UI の [Button](https://material-ui.com/components/buttons/) のプロパティに親コンポーネントの `state` をバインドして変更内容を反映しています。

```js
<Button
  variant={this.state.buttonVariant}
  color={this.state.buttonColor}
>
  {this.state.buttonColor}
</Button>
```

ポイントになるのは子コンポーネントのイベントには親コンポーネントのイベントをバインドする必要がある点です。
親コンポーネントが `setState` 関数を経由して `state` を変更しないと子コンポーネントにその変化が反映されず、再描画されません。

### チェックボックスをチェックしたらテキスト入力欄を表示する
Material-UI の [Checkbox](https://material-ui.com/components/checkboxes/) を利用しています。

```js
<Checkbox
  checked={this.state.showEdit}
  onChange={(e) => this.onChangeCheckbox(e, 'showEdit')}
/>
```

`Checkbox` の `onChange` イベントにバインドしている親コンポーネントのイベント実装は以下。

```js
onChangeCheckbox(e, key) {
  this.setState({[key]: e.target.checked});
}
```

そしてテキスト入力欄の [TextField](https://material-ui.com/components/text-fields/) の実装は以下のようになります。
`this.state.showEdit = true` すなわち、チェックボックスがチェックされている場合だけ描画されます。

```js
{this.state.showEdit ? <TextField label="名前" value={this.state.edit} /> : null}
```

## ヘッダー固定のテーブルの実現方法
[react-table](https://github.com/tannerlinsley/react-table) をはじめ、いくつかテーブルコンポーネントを触ってみたのですがデザインの良さ、シンプルさから、Materil-UIの [Table](https://material-ui.com/components/tables/) が一番しっくりきました。
ただ、 `Table` はヘッダー行の固定が実現されておらず [こちらの書き込み](https://github.com/mui-org/material-ui/issues/6625#issuecomment-381729555) を参考にCSSで補正して実現しています。

```css
.sticky-head {
  background-color: #fff;
  position: sticky;
  top: 0;
}
```
```js
<div style={{height: '400px', overflow: 'scroll'}}>
  <Table>
    <TableHead>
      <TableRow>
        <TableCell className="sticky-head">id</TableCell>
        <TableCell className="sticky-head">label</TableCell>
        <TableCell className="sticky-head">action</TableCell>
      </TableRow>
    </TableHead>
    // 後略
```

[実装の全容はこちら](https://github.com/cobot00/react-trial/blob/review/src/components/forms/ListForm.js)

### スクロール挙動
![sticky-table.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106217/07a45fba-ca73-02eb-8fc1-48958e06fea6.gif)