別途CRUDの記事を書いたら「複雑すぎる」ということなので簡単バージョンを書きます。
radio, checkbox, file等、各種Form要素のコントロールを知りたい人は、複雑?バージョンをご覧下さい。
仕様
下記のような感じ。
CRUDとはいえ、React(SAP)なので、詳細表示、編集、削除は1つのページで実装します。
準備
create-react-app
create-react-appでプロジェクトを作成します。
create-react-app crud-basic
cd crud-basic
必要なモジュールインストール
必要なモジュールをインストールします。
npm install --save bootstrap reactstrap react-router-dom formik yup firebase
yarnならyarn add に置き換えてください。
簡単にモジュールの解説をしておくと、
- bootstrap reactstrap : bootstrap風に見た目を整えるのに使います
- react-router-dom:SPA内でページ遷移(ページ移動)するのに使います
- formik yup : Formの管理とバリデーションについかいます
- firebase : firebaseを利用するのに使います
必要なファイル生成
あらかじめ必要なファイルを生成しておきます。
cd src
touch Firebase.js
mkdir screens
touch screens/Index.js
touch screens/Create.js
touch screens/Detail.js
touch screens/page404.js
Firebase.js
Firebaseを利用するための設定を行います。各自の環境に合わせて設定してください。
import firebase from 'firebase/app';
import 'firebase/firestore';
const firebaseConfig = {
apiKey: "xxxxxxxxxx",
authDomain: "xxxxxxxxxx",
databaseURL: "xxxxxxxxxx",
projectId: "xxxxxxxxxx",
storageBucket: "xxxxxxxxxx",
messagingSenderId: "xxxxxxxxxx",
appId: "xxxxxxxxxx",
measurementId: "xxxxxxxxxx"
};
firebase.initializeApp(firebaseConfig);
export default firebase;
export const db = firebase.firestore();
実装:骨組み作り
CRUD処理を実装するまえにreact-routerでページ遷移できる最低限の記述と動作チェックをしてみます。
Index.js
ただ、「一覧表示」と表示するだけ。新規作成ページへのリンクを貼っておきます。
import React from 'react';
import { Link } from 'react-router-dom';
import firebase, { db } from '../Firebase';
class Index extends React.Component {
render() {
return (
<div className="container">
<h3 className="text-center my-5">一覧表示</h3>
<div className="my-3"><Link to="/create">新規登録</Link></div>
</div>
);
}
}
export default Index;
Create.js
「新規作成」の表示とTopページへ戻るリンクを設置。
import React from 'react';
import { Link } from 'react-router-dom';
class Create extends React.Component {
render() {
return (
<div className="container">
<h3 className="text-center my-5">新規作成</h3>
<div className="text-right my-3"><Link to="/">一覧へ戻る</Link></div>
</div>
);
}
}
export default Create;
Detail.js
「詳細・編集」の表示とTopページへ戻るリンクを設置。
import React from 'react';
import { Link } from 'react-router-dom';
class Detail extends React.Component {
render() {
return (
<div className="container">
<h3 className="text-center my-5">詳細・編集</h3>
<div className="text-right my-3"><Link to="/">一覧へ戻る</Link></div>
</div>
);
}
}
export default Detail;
page404.js
404用のページも作っておきます。
import React from 'react';
import { Link } from 'react-router-dom';
class page404 extends React.Component{
render(){
return(
<div className="container">
<h3 className="text-center my-5">Page not found.</h3>
<div className="text-center"><Link to="/">トップページへ</Link></div>
</div>
);
}
}
export default page404;
App.js
各ファイルができたらルーティングを設定します。
import React from 'react';
import './App.css';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
//screens
import Index from './screens/Index';
import Create from './screens/Create';
import Detail from './screens/Detail';
import page404 from './screens/page404';
class App extends React.Component {
render() {
return (
<BrowserRouter>
<Switch>
<Route exact path="/" component={Index} />
<Route path="/create/" component={Create} />
<Route path="/detail/:uid" component={Detail} />
<Route path="/404" component={page404} />
<Route component={page404} />
</Switch>
</BrowserRouter>
);
}
}
export default App;
動作確認
ここまでできたら、各ページの表示やルーティングがうまくいくか確認してください。
npm start
yarn start
Topページはもちろん、/create, /detail, /404等が表示されるか確認してみてください。
実装:本実装
ではそれぞれのページを実装していきます。
Create.js
データがないと表示できないのでCreate.jsから実装します。
nameとemailだけを入力・登録するFormを設置します。Formikで最低限のバリデーションも付けています。
import React from 'react';
import { Form, FormGroup, Label, Input, Button, FormFeedback } from 'reactstrap';
import { Link } from 'react-router-dom';
import { Formik } from 'formik';
import * as Yup from 'yup';
import firebase, { db } from '../Firebase';
class Create extends React.Component {
//登録ボタンが押されたら
handleOnSubmit = (values) => {
const docId = db.collection("members").doc().id;
db.collection("members").doc(docId).set({
docId: docId,
name: values.name,
email: values.email,
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
});
//登録後、Topに移動
this.props.history.push("/");
}
render() {
return (
<div className="container">
<h3 className="text-center my-5">新規作成</h3>
<div className="text-right my-3"><Link to="/">一覧へ戻る</Link></div>
<Formik
initialValues={{ name: '', email: '' }}
onSubmit={values => this.handleOnSubmit(values)}
validationSchema={Yup.object().shape({
name: Yup.string().required('氏名は必須です。'),
email: Yup.string().email('emailの形式ではありません。').required('Emailは必須です。'),
})}
>
{
({ handleSubmit, handleChange, handleBlur, values, errors, touched }) => (
<Form onSubmit={handleSubmit}>
<FormGroup>
<Label for="name">氏名</Label>
<Input
type="text"
name="name"
id="name"
value={values.name}
onChange={handleChange}
onBlur={handleBlur}
invalid={Boolean(touched.name && errors.name)}
/>
<FormFeedback>
{errors.name}
</FormFeedback>
</FormGroup>
<FormGroup>
<Label for="email">Email</Label>
<Input
type="email"
email="email"
id="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
invalid={Boolean(touched.email && errors.email)}
/>
<FormFeedback>
{errors.email}
</FormFeedback>
</FormGroup>
<Button type="submit">登録</Button>
</Form>
)
}
</Formik>
</div>
);
}
}
export default Create;
Index.js
次に登録されたデータの一覧を作成表示ページを作成します。
import React from 'react';
import { Link } from 'react-router-dom';
import firebase, { db } from '../Firebase';
class Index extends React.Component {
state = {
list: [],
}
//データ取得
getData = async () => {
const colRef = db.collection("members")
.orderBy('createdAt', 'desc')
.limit(10);
const snapshots = await colRef.get();
const docs = snapshots.docs.map(doc => doc.data());
await this.setState({
list: docs,
});
}
//更新時のcalback
onCollectionUpdate = (querySnapshot) => {
//変更の発生源を特定 local:自分, server:他人
// const source = querySnapshot.metadata.hasPendingWrites ? "local" : "server";
// if (source === 'local') this.getData(); //期待した動きをしない
this.getData();
}
componentDidMount = async () => {
//普通に取得
await this.getData();
//collectionの更新を監視
this.unsubscribe = db.collection("members").onSnapshot(this.onCollectionUpdate);
}
//監視解除
componentWillUnmount = () => {
this.unsubscribe();
}
render() {
return (
<div className="container">
<h3 className="text-center my-5">一覧表示</h3>
<div className="my-3"><Link to="/create">新規登録</Link></div>
<table className="table">
<tbody>
{
this.state.list.map(item => (
<tr key={item.docId + String(new Date())}>
<td>{item.docId}</td>
<td>{item.name}</td>
<td>{item.email}</td>
<td><Link to={`/Detail/${item.docId}`}>詳細</Link></td>
</tr>
))
}
</tbody>
</table>
</div>
);
}
}
export default Index;
実装方法の考察
上記実装では、componentDidMount()でgetData()を実行し値を取得すると同時に、onSnapshot()にて変化を監視し、変更が検知された際にもgetData()を実行しています。多くの場合、DidMount時で十分なのですが、これ以降で実装する新規登録や更新処理において、処理完了時にTopページ("/")にリダイレクトしていますが、そのタイミングではまだdb(firestore)に更新が反映されていないため、リスト表示と操作にギャップが発生するのうめるために、firestoreでの反映が完了した時点で、再度getData()が行われるようにしています。
この実装はこのサンプルの範囲ではうまく稼働しますが、他人による更新がリアルタイムに反映されたり、検索やページネーション等の機能を加えた際、よきせぬ動きになる場合があります。目的に応じて更新ロジックを変更する必要があります。
Detail.js
次に、詳細表示、更新、削除機能を持ったページを作成します。
Create.jsをベースに以下のようにしました。
ポイントは、FormikのinitialValuesに検索結果をセットしているところです。また、そのためにenableReinitializeをtrueにしています。
<Formik
enableReinitialize
initialValues={{ name: this.state.member.name, email: this.state.member.email }}
以下、実装です。
import React from 'react';
import { Form, FormGroup, Label, Input, Button, FormFeedback } from 'reactstrap';
import { Link } from 'react-router-dom';
import { Formik } from 'formik';
import * as Yup from 'yup';
import firebase, { db } from '../Firebase';
class Detail extends React.Component {
state = {
member: { name: '', email: '' }
}
//更新ボタンが押されたら
handleOnSubmit = (values) => {
db.collection("members").doc(this.props.match.params.uid).update({
name: values.name,
email: values.email
});
//Topに移動
this.props.history.push("/");
}
//uidで指定したメンバーの値を取得
getMember = async (uid) => {
const docRef = db.collection("members").doc(uid);
const doc = await docRef.get();
//ドキュメントの存在確認
if (doc.exists) {
this.setState({
member: doc.data(),
});
}else{
//なければ404ページへ
this.props.history.push("/404");
}
}
//delete
handleDelete = (uid) => {
if (window.confirm('削除しますか?')) {
db.collection("members").doc(uid).delete();
this.props.history.push("/");
}
}
//値を取得
componentDidMount = () => {
this.getMember(this.props.match.params.uid);
}
render() {
return (
<div className="container">
<h3 className="text-center my-5">詳細・編集</h3>
<div className="text-right my-3"><Link to="/">一覧へ戻る</Link></div>
<Formik
enableReinitialize
initialValues={{ name: this.state.member.name, email: this.state.member.email }}
onSubmit={values => this.handleOnSubmit(values)}
validationSchema={Yup.object().shape({
name: Yup.string().required('氏名は必須です。'),
email: Yup.string().email('emailの形式ではありません。').required('Emailは必須です。'),
})}
>
{
({ handleSubmit, handleChange, handleBlur, values, errors, touched }) => (
<Form onSubmit={handleSubmit}>
<FormGroup>
<Label for="name">氏名</Label>
<Input
type="text"
name="name"
id="name"
value={values.name}
onChange={handleChange}
onBlur={handleBlur}
invalid={Boolean(touched.name && errors.name)}
/>
<FormFeedback>
{errors.name}
</FormFeedback>
</FormGroup>
<FormGroup>
<Label for="email">Email</Label>
<Input
type="email"
email="email"
id="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
invalid={Boolean(touched.email && errors.email)}
/>
<FormFeedback>
{errors.email}
</FormFeedback>
</FormGroup>
<Button type="submit" color="success">更新</Button>
</Form>
)
}
</Formik>
<div className="my-3">
<Button color="danger" onClick={() => this.handleDelete(this.props.match.params.uid)}>削除</Button>
</div>
</div>
);
}
}
export default Detail;
簡単ではありますが、以上です。