8.5 こんな時どうする?
8.5.1 洗練されたGUIを作る
Reactでは、色々なコンポーネントを自分で作ることができますが、現在、主流になっている操作性や見た目が良いユーザーインタフェースを自分で一から作るのはとても大変です。それなりのGUIを作るためには、サードパーティーが提供している、「UIコンポーネントライブラリー」を使うのが一般的です。
例えば、以下のようなコンポーネントが提供されます。
- 綺麗にデザインされたボタン、フォーム、表などの基本的なUIコンポーネント
- ナビゲーションメニュー、ドロップダウンメニューなどのメニュー
- アラート表示、タブ表示、カード表示などの表示エリア用コンポーネント
- 処理を待っている間に表示されるスピナー
以下は、現在、よく使われている(と思われる)React用のUIコンポーネントライブラリの例です。
- Material-UI (MUI)
- googleによって提唱されているデザインガイドライン「Material Design」に基づいたUIを提供
- 豊富なコンポーネント
- カスタマイズ性が高い
- Ant Design (Ant-D)
- 柔軟なスタイリングが可能で、カスタマイズが簡単
- 軽量で直感的な API
- React Bootstrap
- 歴史としては一番長いので、ドキュメントなど情報が豊富
- レスポンシブデザイン(スマホ、タブレット、PCなど異なる画面幅に合わせて自動調整するWebデザイン手法)への対応
このテキストでは、React Bootstrapを使った例で説明しますが、他のコンポーネントライブラリを使うのもありです。何年か前まではReact Bootstrapが一番使われているコンポーネントライブラリでしたが、現在では(おそらく)Material-UIがトップシェアになっているのではないかと思います。コンポーネントライブラリは移り変わりが早く、今後何が主流になっていくのかを見ながら、どれを使うのかを決めていくのが良いかと思います。
React Bootstrapを使うために、npmを使って、react-bootstrap と、これが依存しているbootstrapをインストールしておきます。
npm install react-bootstrap bootstrap
その他、bootstrap-iconsというボタンなどに表示するアイコンライブラリ(https://icons.getbootstrap.com)も一揃えあります。以下のコマンドでインストールしておきます。
npm i bootstrap-icons
8.5.2 操作パネルからメイン画面を切り替える
下図のように、「操作パネル」を設け、これの操作により「メイン画面」の表示を変えたり変更したりというのはよくあるGUIのパターンです。このようなGUIの実現方法を説明します。

このパターンは、Reactの以下の機能を使って実現します。
- propsによって関数をコンポーネントに渡す
- propsによって値を渡す
- useState()による状態変数によって、画面を切り替える
Reactではpropsを使ってコンポーネントに値を引き渡すことができることはすでに説明しましたが、値だけでなく関数も引き渡すことができます。以下のサンプルコードを見てください。
/* ControlPanelPage.js */
import React, {useState} from 'react';
import ControlPanel from './ControlPanel';
import TeamList from '../components/TeamList';
import './ControlPanelPage.css';
function ControlPanelPage(){
// 状態変数:子コンポーネント(ControlPanel)から更新され、
// 更新された値は子コンポーネント(TeamList)に引き渡される
const [league_value, setLeague] = useState(null);
return(
<div className="app_page">
<div className="sidebar">
<div className="cntpanel">
{/* <ControlPanel handler={setParams}/> */}
<ControlPanel league_handler={setLeague} />
</div>
</div>
<div className="contents">
<TeamList league={league_value} />
</div>
</div>
)
}
export default ControlPanelPage;
親コンポーネントであるControlPanelPageでは、useState()を使い、状態変数’league_value’とその値を設定する関数‘setLeague’が定義されています。
setLeagueは、以下の行で子コンポーネントControlPanelに’league_handler’という名前で引き渡されています。
<ControlPanel league_handler={setLeague} />
こうすることによって、ControlPanelコンポーネントからsetLeagu関数を使うことができるようになります。つまり、ControlPanelコンポーネントから親コンポーネントが持つ状態変数league_valueを変更できるようになります。
関数を渡されたControlPanelのコードは以下です。
/* ControlPanel.js */
import Button from 'react-bootstrap/Button';
function ControlPanel(props){
const leagueHandler = props.league_handler;
const setAction = (e) =>{
leagueHandler(e.target.value);
}
return(
<div className="teams_viewer">
<div className="teams_viewer_panel">
<select id="select_league" onChange={(e)=>setAction(e)}>
<option value="1"> J-1 </option>
<option value="2"> J-2 </option>
<option value="3"> J-3 </option>
</select>
</div>
</div>
)
}
export default ControlPanel;
- const leagueHandler = props.league_handler; の行で渡された関数は、’leagueHandler’という変数に格納され’leagueHandler’という関数名で実行できるようになります。
- select要素のハンドラーsetActionでleagueHandlerが呼び出され、selectで選ばれたアイテムの値が親コンポーネントの状態変数league_valueにセットされます。
- league_valueの状態が変わると、親コンポーネントのレンダーが呼び出され、TeamListに変更されたleague_valueの値が引き渡されます。
- これにより、TeamListのレンダーが呼び出されTeamListの表示が更新されます。

8.5.3 ページネーション
Djangoによるバックエンドと、Reactによるfrontendを併せてページネーションGUIを実現する方法を解説します。Django側のコードは以下で、MyPaginationというページネーションクラスを使うことにします。
from rest_framework import generics
from rest_framework.pagination import PageNumberPagination
class MyPagination(PageNumberPagination):
page_size = 10
def get_paginated_response(self, data):
return Response({
'current' :self.page.number, # 現在のページ
'count': self.page.paginator.count, # 項目数の合計
'final': self. page.paginator.num_pages, # 全体のページ数
'next': self.get_next_link(), # 次のページネーションへのリンク
'previous': self.get_previous_link(), # 前のページネーションへのリンク
'results': data, # 結果データ (page_size個のデータ)
})
class TeamListView(generics.ListAPIView):
serializer_class = TeamsSerializer
pagination_class = MyPagination # 追加
def get_queryset(self):
# URLからリーグIDを取得してフィルタ
league_id = self.kwargs['league_id']
return Teams.objects.filter(league_id=league_id)
#return get_list_or_404(Teams, league_id=league_id)
これに対し、React側のコードが以下です。
import React, { useState, useEffect, useContext } from 'react';
import TeamList from './TeamList';
import Button from 'react-bootstrap/Button';
function TeamsViewer(props){
const [data, setData] = useState(null);
const getPage = (target) =>{
// targetで指定されたURLに対してGETリクエストを送り、
// 結果をdataにセット
console.log(target);
if (target){
fetch(target)
.then(response => {
return response.json();
})
.then(result =>{
console.log(result);
setData(result);
})
.catch(error =>{
console.error('----Error---');
console.error(error);
})
}
}
useEffect(() => {
if(props.league){
getPage("http://127.0.0.1:8000/api/v1/teams/" + props.league);
}
},[props])
const handleClick = (item) =>{
if (data[item]){
getPage(data[item]);
}
}
if (data){
return(
<div className="Viewer">
<div className="header_box">
<div className="pagination_box">
<Button variant="light" size="sm"
onClick={() => handleClick('previous')} id="pvB">
<i class="bi bi-caret-left-fill"></i>
</Button>
<i>
{data['current']}/{data['final']}
</i>
<Button variant="light" size="sm"
onClick={() => handleClick('next')} id="nxB">
<i class="bi bi-caret-right-fill"></i>
</Button>
</div>
</div>
<div>
<TeamList list_data={data['results']} />
</div>
</div>
)
}
}
export default TeamsViewer;
<div className="pagination_box"> ... </div>の部分に注目してください。以下のように表示され、バックエンドから帰ってきた値やハンドラーが紐付けられます。

この「ページネーションボックス」の矢印ボタンの操作によって、バックエンドにGETリクエストが送られ、得られた値の’results’が最終的にTeamListに引き渡されてTeamListの表示が更新されます。
TeamListのコードは以下のように、引き渡された値をそのまま表示するようになっています。
import React, { useState, useEffect } from 'react';
function TeamList(props){
if(props.list_data.length > 0){
return (
<div className="team_list">
<table>
<thead>
<tr>
<th>チーム名</th>
<th>ロゴ</th>
</tr>
</thead>
<tbody>
{props.list_data.map((team, index)=>{
return(
<tr key={index}>
<td>{team['team_name']}</td>
<td> <img src={team['team_logo']} /> </td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}
}
export default TeamList;
8.5.4 メニュー選択でページを切り替える
ナビゲーションメニューを上部に設け、これによりページを切り替える典型的なGUIの基本的な実現方法をサンプルコードで説明します。

メニューはreact-bootstrapのナビゲーションメニュー関連のコンポーネントを使うことにします。
また、ページの動的な切り替えを行うためには、Reactに「ルーティング」の機能を追加してやる必要があり、そのためにreact-router-domというサードパーティーモジュールをインストールします。
npm install react-router-dom
また、切り替えるページを表示するコンポーネントとして、PageA, PageB, PageC1, PageC2の4つのコンポーネントが用意されているものとします。
まず、親コンポーネントであるMenuSamplePageのコードを見てみます。
import {BrowserRouter, Routes, Route } from 'react-router-dom';
import NavigationMenu from './NavigationMenu';
import PageA from './PageA';
import PageB from './PageB';
import PageC1 from './PageC1';
import PageC2 from './PageC2';
import './MenuSamplePage.css';
import 'bootstrap/dist/css/bootstrap.min.css';
function MenuSamplePage(){
return(
<div>
<BrowserRouter>
<NavigationMenu />
<Routes>
<Route path="/page_a" element={<PageA />} />
<Route path="/page_b" element={<PageB />} />
<Route path="/page_c1" element={<PageC1 />} />
<Route path="/page_c2" element={<PageC2 />} />
</Routes>
</BrowserRouter>
</div>
);
}
export default MenuSamplePage;
ここで、ルーティングの設定が行われています。ルーティングとは、送られてきたURLに基づいて表示するコンポーネントを決定することです。
まず、<BrowserRouter>...</BrowserRouter>でアプリケーション全体をラップすることによりルーティング機能が使えるようになります。
<Routes>...</Routes>で囲まれたエレメントの中にルーティングルールが記述されています。
例えば、以下は、/page_aというURLが送られてきたらPageAのコンポーネントを子のelementとして表示するという意味です。
<Route path="/page_a" element={<PageA />} />
これにより<Routes>...</Routes>の部分は<PageA />に置き換えられます。
一方で、NavigationMenuの方は以下のようになっています。
import Container from 'react-bootstrap/Container';
import Nav from 'react-bootstrap/Nav';
import Navbar from 'react-bootstrap/Navbar';
import NavDropdown from 'react-bootstrap/NavDropdown';
function NavigationMenu(props){
return(
<Navbar expand="lg" className="bg-body-tertiary" data-bs-theme="dark">
<Container>
<Navbar.Brand href="/page_a">Sample</Navbar.Brand> {/* 製品名、ロゴなどを表示*/}
<Navbar.Toggle aria-controls="basic-navbar-nav" /> {/* 一定幅を超えるとハンバーガーメニュー表示 */}
<Navbar.Collapse id="basic-navbar-nav"> {/* 一定幅を超えると子要素を非表示に */}
<Nav className="mr-auto">
<Nav.Link href={"/page_a"}>ページA</Nav.Link>
<Nav.Link href={"/page_b"}>ページB</Nav.Link>
<NavDropdown title="ページC" id="activities-dropdown">
<NavDropdown.Item href={"/page_c1"}>ページ C-1</NavDropdown.Item>
<NavDropdown.Item href={"/page_c2"}>ページ C-2</NavDropdown.Item>
</NavDropdown>
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
);
}
export default NavigationMenu;
<Nav.Link> や <NavDropdown.Item> のhrefでクリックされたら呼び出すURLを定義しています。URL呼び出しが行われると、これがBrowerRouterのルーティングルールによって解釈され<Route>で指定されたコンポーネントが表示されます。
Webサーバーにデプロイするための修正
npm start を使ってReactの開発用サーバーを使って表示を行う場合は、上のコードでも良いのですが、本番用ののWebサーバーにReactのプログラムをデプロイして使う際には、これではうまくいきません。
8.5.5 ファイルのアップロード
Djangoによるバックエンドと、Reactによるfrontendを併せてファイルのアップロードを実現する方法を解説します。
バックエンド側の仕組みは、「7.3.8 ファイルをアップロードしたい」で説明していますので、こちらのサンプルプログラムと組み合わせて使うReactのプログラム例を示します。
このプログラムでは、アップロードするファイルを、ドラッグ&ドロップで実現するようにします。以下のように、ファイルをドロップする「ドロップエリア」を表示します。

ファイルをドロップすると下のようなプレビュー画面が表示されます。ここでキャプションの文字列を入れて「送信」ボタンを押すと、ファイルがアップロードされその結果がデータベースに反映されます。

ドラッグ&ドロップの機能は、「react-dropzone」というサードパーティーライブラリを使って実現しますので、これをインストールしておきます。
% npm install react-dropzone
また、プレビュー画面は現在表示中の画面(親画面)の上に重なって表示される「モーダルウインドウ」にしています。これはreact bootstarpのModalコンポーネントを使って実現します。
まず、ドロップエリアのプログラムです。
// FileDripBox.js
import { useCallback , useMemo, useState} from 'react';
import { useDropzone } from 'react-dropzone';
import FileUploader from './FileUploader' ;
import './FileDropBox.css';
// ドロップエリアの形状を定義
// baseStyle : ドロップエリアの基本的なスタイル
// borderNormalStyle : 通常時の枠線
// borderDragStyle : ドラッグしたマウスカーソルがドロップエリア上に来た時の枠線
const baseStyle = {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
width: "40%",
height: 80,
};
const borderNormalStyle = {
border: "2px dotted #888",
backgroundColor: "rgba(255, 255, 255, 0.95)"
};
const borderDragStyle = {
border: "3px solid #00f",
transition: 'border .5s ease-in-out',
backgroundColor: "rgba(255, 255, 255, 0.45)"
};
function FileDropBox(props) {
const [target_file, setFile] = useState(null);
const [isVisible, setVisibility] = useState(false);
const closeModal = ()=>{
setVisibility(false);
}
//ドロップエリアにファイルがドロップされた時に呼ばれるコールバック関数
const onDrop = useCallback((acceptedFiles) => {
// Do something with the files
//console.log('acceptedFiles:', acceptedFiles);
let file = acceptedFiles[0];
console.log(file);
setFile(file);
setVisibility(true);
}, []);
// 必要なハンドラーをバインドして、ドロップエリアでこれらを使う
const { getRootProps, getInputProps, isDragActive, acceptedFiles }
= useDropzone({onDrop});
// ラッグしたマウスカーソルがドロップエリア上に出たり入ったり来たりする時
//(isDragActiveが変化した時)に動的にスタイルをセットし直す
const style = useMemo(() => (
{ ...baseStyle, ...(isDragActive ? borderDragStyle : borderNormalStyle)}
), [isDragActive]);
return (
<div className="drop_panel">
<div {...getRootProps({style})}>
<input {...getInputProps()} />
{
isDragActive ?
<p>Drop the files here ...</p> :
<p>Drag 'n' drop some files here, or click to select files</p>
}
</div>
<FileUploader target={target_file} show={isVisible} handler={closeModal} />
</div>
)
}
export default FileDropBox;
return()の中の最後の要素に<FileUploader>が定義されていますが、これはModalウインドウのコンポーネントです。これに渡されている属性showがfalseの時には表示されず、trueの時に表示されます。
FileUploaderのプログラムは以下です。
// FileUploader.js
import Button from 'react-bootstrap/Button';
import Modal from 'react-bootstrap/Modal';
function FileUploader(props){
const closeModal = props.handler;
// ファイルに関連する情報をバックエンドに送信する
const sendForm = ()=>{
const formData = new FormData();
let element = document.querySelector("#caption");
formData.append('picture', props.target);
formData.append('caption', element.value);
let target = "http://127.0.0.1:8000/api/v1/picture_upload/"
fetch(target,{
method : 'POST',
credentials: "same-origin",
body : formData,
})
.then(response => {
closeModal();
})
.catch(error =>{
console.error(error);
closeModal();
})
}
if(props.target){
return (
<div
className="modal show"
style={{ display: 'block', position: 'initial' }}
>
<Modal show={props.show} onHide={closeModal} centered>
<Modal.Header closeButton>
<Modal.Title>ファイルの送信</Modal.Title>
</Modal.Header>
<Modal.Body className="modal_contents">
<div> {props.target.name} </div>
<img src={URL.createObjectURL(props.target)} width="20%" height="20%" />
<div className="caption_box">
<div>キャプション</div>
<input type="text" id="caption" style={{marginLeft: "10px", width: "160px"}}></input>
</div>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={closeModal}>キャンセル</Button>
<Button variant="primary" onClick={sendForm}>送信</Button>
</Modal.Footer>
</Modal>
</div>
);
}
}
export default FileUploader;
sendForm関数では、バイナリファイルを送信するために「Form」を使った送信を行います。Formを作り、内容をappend()でセットし、これをHTTPリクエストのbodyに入れてバックエンドに送信します。
8.5.6 Rest APIを呼び出す(fetch関数)
DRFなどで提供されるRest APIを呼び出すには、fetchというWebブラウザに組み込まれているHTTP リクエストを行い、レスポンスを処理するための APIを使います。
fetchは非同期処理を行う非同期関数です。
- 同期処理:タスクを1つずつ順に実行し、完了を待って次へ進む方式
- 非同期処理:時間のかかるタスク(通信等)の完了を待たずに次のタスクを実行する方式
同期処理でRest APIの処理を行うと、バックエンドから応答が返ってくるまでの間、何も処理が実行できない待ち状態が発生してしまい、待ち状態の間アプリや画面がフリーズしたようになってしまいます。このためインターネットを介して応答を待つ処理は、応答を待っている間にも他の処理(例えば、マウス入力の受付や画面の描画など)を行うことができるよう、非同期処理にするのが一般的です。
fetch関数の処理はブラウザがバックグラウンドで実行します。fetchは呼ばれると、処理をバックグラウンドに引き渡しpromiseというオブジェクトを返します。promiseは’status’と’result’という変数を持っています。statusの値は以下のいずれかになります。
- 待機 (pending): 初期状態。成功も失敗もしていません。
- 履行 (fulfilled): 処理が成功して完了したことを意味します。
- 拒否 (rejected): 処理が失敗したことを意味します。
バックグラウンド処理が終わり、成功した時にはstatusは’fullfiled’に、失敗した時には’rejected’に変わります。成功した時の実行結果はresult変数に格納されます。
fetchが呼ばれた後、その結果を待つことなく次の処理が実行されます。それでは、fetchの結果をどうやって処理すれば良いのでしょう?
実は、promiseは結果が得られた時に呼び出されたされる「コールバック関数」を持つことができます。このコールバック関数の中に結果を処理するコードを書き、この関数をpromiseに登録します。
状態がfulfilledまたはrejectedになったときに呼び出されるコールバック関数をそれぞれ登録することができます。
- then( callback_fullfiled) : promiseが成功した場合に呼び出すコールバック関数を登録します。
- catch(callback_rejected) : promiseが失敗した場合に呼び出すコールバック関数を登録します。
これらのメソッド自体もプロミスを返すので、次のように連結することができます。
my_promise
.then(handler1).
.then(handler2).
.catch(handler3);
rejectedの状態のpromiseから呼ばれたthenのハンドラーは、元のプロミスと同じ状態のものを返します。最初のpromiseがrejectedの状態であれば、次のthen()は同じrejectedのプロミスを返し、さらに次のthen()もrejectedのpromiseを返します。このpromiseにはcatch()が紐づけられているので、handler3が実行されます。このように、上記のコードでは連鎖の中のどこでエラーが起きても、最終的にはエラー処理handler3が実行されます。
以下は、fetchを使った実際の処理の例です。
fetch(target,{
credentials: "same-origin",
})
.then(response => {
return response.json();
})
.then(result =>{
setInfo(result);
})
.catch(error =>{
console.error(error);
})
この処理は、次のように非同期で処理されます。
