概要
create-react-app を利用した基本的なSPAの実装サンプルの共有です。
メニュー表示と認証、イベント処理などを網羅的に扱っているサンプルがなかったので後世のために記事にしてみます。アプリケーションの形態としては管理機能が近いかと。
アプリケーション外観
ソースコードはGitHubの react-trial にて公開しているので参考にしてください。
なお、ReactやMateril-UIの扱いに至らぬところがあれば指摘をいただけるとありがたいです。
この記事で触れる事項
- 認証の前後でのメニュー表示制御、リダイレクト制御
- セッション管理
- 入力内容に応じて関連する項目の表示を変更する
- ヘッダー固定のテーブルの実現方法
- Materil-UIの基本的な使い方
この記事で触れない事項
Reactに固有の話ではない一般的な事項なのでこの記事では触れません。
- 認証処理
- APIリクエストの実行とレスポンスの処理
前提条件
- Reactのバージョン 16.8.6
- 認証状態、セッション管理に Context API と localStorage を利用しています
- ルーティングは react-router 5.0.0 を利用しています
- ES6以降の記法でコードは記述しています
認証の前後でのメニュー表示制御、リダイレクト制御
認証前の表示
認証後の表示
解説
認証されているかどうかで表示するメニューとルーティングを以下のようなコードで切り替えています。
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>
);
};
認証前は以下のようなルーティングで /login
以外に遷移しようとすると /login
にリダイレクトします。
const NotAuthenticatedRouting = () => {
return (
<Switch>
<Route path="/login" component={LoginForm} />
<Route path='*'
render={() => (
<Redirect to="/login" />
)}
/>
</Switch>
);
};
認証後は以下のルーティングが適用され、表示されるメニューに対応する画面が表示されるようになります。
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
と独自のグローバルなセッション管理オブジェクトを組み合わせて認証状態・セッション管理を行っています。
export const SessionContext = React.createContext({
authenticated: false,
login: () => {},
logout: () => {}
});
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
の認証状態が反映され、以降のメニュー表示やルーティングの制御に伝播されていきます。
this.state = {
authenticated: sessionState.authenticated
};
localStorageのセッションをReactの実装に反映する
入力内容に応じて関連する項目の表示を変更する
セレクトボックスの内容に応じてボタンの表示を変更する処理とチェックボックスのチェックの有無でテキスト入力欄の表示・非表示を切り替える処理の紹介です。
実装の全容はこちら
初期状態
変更後
セレクトボックスの選択内容でボタンの外観を変更する
Material-UI の Select コンポーネントを利用しています。
セレクトボックスの onChange
イベントに親コンポーネントの onChangeSelect
イベントをバインドして親コンポーネントの state
を変更するようにしています。
<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>
onChangeSelect(e, key) {
this.setState({[key]: e.target.value});
}
Material-UI の Button のプロパティに親コンポーネントの state
をバインドして変更内容を反映しています。
<Button
variant={this.state.buttonVariant}
color={this.state.buttonColor}
>
{this.state.buttonColor}
</Button>
ポイントになるのは子コンポーネントのイベントには親コンポーネントのイベントをバインドする必要がある点です。
親コンポーネントが setState
関数を経由して state
を変更しないと子コンポーネントにその変化が反映されず、再描画されません。
チェックボックスをチェックしたらテキスト入力欄を表示する
Material-UI の Checkbox を利用しています。
<Checkbox
checked={this.state.showEdit}
onChange={(e) => this.onChangeCheckbox(e, 'showEdit')}
/>
Checkbox
の onChange
イベントにバインドしている親コンポーネントのイベント実装は以下。
onChangeCheckbox(e, key) {
this.setState({[key]: e.target.checked});
}
そしてテキスト入力欄の TextField の実装は以下のようになります。
this.state.showEdit = true
すなわち、チェックボックスがチェックされている場合だけ描画されます。
{this.state.showEdit ? <TextField label="名前" value={this.state.edit} /> : null}
ヘッダー固定のテーブルの実現方法
react-table をはじめ、いくつかテーブルコンポーネントを触ってみたのですがデザインの良さ、シンプルさから、Materil-UIの Table が一番しっくりきました。
ただ、 Table
はヘッダー行の固定が実現されておらず こちらの書き込み を参考にCSSで補正して実現しています。
.sticky-head {
background-color: #fff;
position: sticky;
top: 0;
}
<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>
// 後略