Firebase Realtime DatabaseをAngularで使ってみる
はじめに
前回Reactで使ってみたので今回はAngularでやってみる。
Firebaseの設定は前回と同じため、今回はプロジェクト作るところから始めていく。
(Firebaseの設定については前回参照)
TL;DR.
実装していく
プロジェクト作成
angular cliらスタート。
せっかくなのでv10から追加されたstrictモードを使ってみる。
$ npx @angular/cli new realtime-db-sample-with-angular --strict
ライブラリのインストール
公式から提供されているライブラリである[AngularFire](https://github.com/angular/angularfire)
があるのでそちらをインストールする。
ログイン周りを実装するときにSDKが必要になるので、こちらもインストールしておく。
$ npm run ng -- add @angular/fire $ npm install --save firebase
Firebase周り
Firebaseの操作に関する部分は表示系とは直接関係無い+汎用的なものになると思うのでライブラリ化しておく。
$ npm run ng -- g library firebase-library
初期化処理
Quick Startを参考に初期化処理周りをサクッと追加する。
コンフィグ周りを個別のファイルに切り出して、
import { FirebaseOptions } from '@angular/fire'; export const firebaseConfig: FirebaseOptions = { production: false, firebase: { // コピペ } };
それをfirebase-library.module.ts
で初期化のときに読み込む。
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AngularFireModule } from '@angular/fire'; import { AngularFireDatabaseModule } from '@angular/fire/database'; import { FirebaseUsecaseService } from './service/firebase-usecase.service'; // ↑で作ったconfigファイル import { firebaseConfig } from './config/config'; import { LoginComponent } from './components/login/login.component'; import { AngularFireAuthModule } from '@angular/fire/auth'; import { FirebaseFormatterService } from './service/firebase-formatter.service'; @NgModule({ declarations: [], imports: [ // 初期化のときにconfigを渡してあげる AngularFireModule.initializeApp(firebaseConfig.firebase), // RealtimeDatabaseを使うので必要なmoduleをimport AngularFireDatabaseModule, // ログイン周りに必要なmoduleをimport AngularFireAuthModule ], providers: [], exports: [] }) export class FirebaseLibraryModule { }
読み書きするserviceを作成
CRUD周りの操作を行うサービスを作成。
こちらも公式サンプルを参考にしながら進めていく。
まずはCLIで生成するところから。
$ npm run ng -- g service firebse-usecase --project=firebase
CRUDを進めていくが基本的にはReactでやった時と同じため、
今回は全件取得、登録、削除のみを実装することにする。
import { Injectable } from '@angular/core'; import { AngularFireDatabase, SnapshotAction } from '@angular/fire/database'; import { Observable } from 'rxjs'; import { map, take } from 'rxjs/operators'; import { FirebaseFormatterService } from './firebase-formatter.service'; import { FirebaseKeyValue } from '../types/firebase-types'; @Injectable({ providedIn: 'root' }) export class FirebaseUsecaseService { private items: Observable<SnapshotAction<string>[]>; constructor(private readonly db: AngularFireDatabase, private readonly formatter: FirebaseFormatterService) { // 指定したURL以下の値がリスト形式で取得される // 絞りたければ渡すパスを絞っていけば良い this.items = this.db.list<string>('/sample').snapshotChanges(); } /** * 全件取得する */ fetchDocumentAll() { // 余分なパラメータが多いので、使いやすい形式に整形する return this.items.pipe(map(this.documentToResponse)); } /** * 登録を行う */ async setDocument(registerKeyValue: FirebaseKeyValue) { // setでの更新は上書きになってしまうので、登録済みのデータを取得しておく // 今回は値を取得してから後続処理に移りたいため、Promiseに変換して取得を待つ const item = await this.fetchDocumentAll().pipe(take(1)).toPromise(); // 登録したいデータと登録されているデータをマージしつつ整形する const registerDocument = this.objectToDocument([...item, registerKeyValue]); // sampleのデータを上書き登録 this.db.object('/sample').set(registerDocument); } /** * 指定したパス配下のデータを全て削除する */ deleteAll() { this.db.object('/sample').remove(); } /** * 取得したデータからkey,valueの値のみを取り出す */ private documentToResponse(document: SnapshotAction<string>[]): FirebaseKeyValue[] { return document.map(item => ({ key: item.key, value: item.payload.val() })); } /** * 登録用のデータに整形する */ private objectToDocument(keyValues: FirebaseKeyValue[]): FirebaseDocument { // 登録したいデータは{[key: string]: value}形式なので整形 return keyValues.reduce((previous, current) => { // keyが無い場合は今回考慮しない const registereData = {[current.key!]: current.value}; return {...previous, ...registereData}; }, {}); } }
ログインコンポーネント
登録、削除はログイン済みの場合のみ可能としているので、ログイン処理を実装していく。
ログイン処理を行うコンポーネントは作成したライブラリ側に用意する。
$ npm run ng -- g component components/login --project=firebase-library
こちらも公式のサンプルを参考にしながら進めていく。
まずはts側から。
import { Component } from '@angular/core'; import { AngularFireAuth } from '@angular/fire/auth'; import { Observable } from 'rxjs'; import { auth, User } from 'firebase/app'; @Component({ selector: 'lib-login', templateUrl: './login.component.html', styleUrls: ['./login.component.scss'] }) export class LoginComponent { private _user: Observable<User | null> constructor(private auth: AngularFireAuth) { this._user = this.auth.user; } login() { // Googleログインを行う this.auth.signInWithPopup(new auth.GoogleAuthProvider()); } logout() { // ログアウト this.auth.signOut(); } get user() { return this._user; } }
次にHTML側を書いていく。
user情報が取得できていれば、ログアウトボタンを表示。
できていなければログインボタンを表示する。
<ng-container *ngIf="user | async; else showLoginButton"> <button (click)="logout()">ログアウト</button> </ng-container> <ng-template #showLoginButton> <button (click)="login()">ログイン</button> </ng-template>
app.moduleに追記
ここまでで作ったlibraryを使えるようにapp.module
に追記する。
import { ReactiveFormsModule } from '@angular/forms'; import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { ListComponent } from './components/list/list.component'; import { FirebaseLibraryModule } from 'firebase-library'; import { FormComponent } from './components/form/form.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, // これを追加する FirebaseLibraryModule, // ルーティングを使いたいのでimportしておく AppRoutingModule, // 後でリアクティブフォームを使いたいので、importしておく ReactiveFormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
取得結果表示ページ作成
ここから表示系を準備していく。
まずは取得結果を表示するページを作成する。
CLIでcomponentを作るところから。
$ npm run ng -- g component components/list
ts側は下記の通り。
作ったlibraryを使い全件取得→結果を表示するだけのcomponentにする。
import { Component } from '@angular/core'; import { FirebaseUsecaseService } from 'firebase-library'; import { BehaviorSubject } from 'rxjs'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'] }) export class ListComponent { private items$ = new BehaviorSubject<any[]>([]); constructor(private readonly firebase: FirebaseUsecaseService) { firebase.fetchDocumentAll().subscribe(res => { this.items$.next(res); }, err => { console.log('error', err); }); } get items() { return this.items$; } }
HTML側は↓の通り。
itemsはBehaviorSubject
なので、asyncパイプを使って表示させてあげる。
<dl> <ng-container *ngFor="let item of items | async"> <dt>key: {{item.key}}</dt> <dt>value: {{item.value}}</dt> </ng-container> </dl>
登録、削除ページ作成
サクッとcomponentを作るところから始める。
$ npm run ng -- g component components/form
登録するデータを入力する箇所はリアクティブフォームで作ってみる。
リアクティブフォームについては昔メモしたのでそちらを参考に。
import { FormGroup, FormControl, Validators } from '@angular/forms'; import { Component } from '@angular/core'; import { FirebaseUsecaseService } from 'firebase-library'; @Component({ selector: 'app-form', templateUrl: './form.component.html', styleUrls: ['./form.component.scss'] }) export class FormComponent { // key、valueはどちらも必須とする readonly formGroup = new FormGroup({ key: new FormControl('', [Validators.required]), value: new FormControl('', [Validators.required]) }); constructor(private readonly firebase: FirebaseUsecaseService) { } registerData() { // setDocumentはasync functionだけど、今回は待つ必要が無いので呼び出しっぱなし this.firebase.setDocument({key: this.key?.value, value: this.value?.value}); } deleteAll() { this.firebase.deleteAll(); } get key() { return this.formGroup.get('key'); } get value() { return this.formGroup.get('value'); } }
HTML側は下記の通り。
ここでログインボタンを読み込んでおく。
<div> <!-- 作成したログイン/ログアウトボタンコンポーネント --> <lib-login></lib-login> </div> <form [formGroup]="formGroup"> <label>key: <input type="text" formControlName="key"/></label> <!-- 必須項目未入力のときのエラーメッセージ --> <div *ngIf="key?.invalid && (key?.dirty || key?.touched)"> <span *ngIf="key?.hasError('required')">必須です。</span> </div> <label>value: <input type="text" formControlName="value"/></label> <!-- 必須項目未入力のときのエラーメッセージ --> <div *ngIf="value?.invalid && (value?.dirty || value?.touched)"> <span *ngIf="value?.hasError('required')">必須です。</span> </div> <button (click)="registerData()">登録</button> </form> <button (click)="deleteAll()">全消し</button>
ルーティングの設定
一覧表示と登録、削除を一応ページ分ける。
ルーティングの設定が必要なのでサクッと実装しておく。
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { ListComponent } from './components/list/list.component'; import { FormComponent } from './components/form/form.component'; const routes: Routes = [ {path: 'list', component: ListComponent}, {path: 'form', component: FormComponent} ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
動作確認
実装が終わったので動作確認していく。
先にライブラリのビルドが必要なのでまずはそこから。
$ npm run build -- firebase-library
ビルドが無事成功したらアプリを起動する。
$ npm start
起動できたらhttp://localhost:4200/list
にアクセスしてみる。
画像の取得結果を表示できていればOK。
次にhttp://localhost:4200/form
にアクセスしてみる。
画像の様にログインボタン、登録フォーム、削除ボタンが表示されていればOK。
まとめ
今回はAngularでRealtimeDatabaseを使ってみた。
公式からライブラリが提供されているのもあり、結構簡単に実装できたと思う。
さっくり作りすぎてページ分けとか微妙だったので、もうちょっときれいに作っても良かった気がする。
公式ライブラリのおかげでとても触りやすかったので、今後なにか作っていきたいところ。
参考リンク
Firebase Realtime DatabaseをReactで使ってみる〜書き込み編〜
はじめに
前回読み込みについてまとめたので、今回は書き込み周りについてまとめていく。
基本的なFirebaseの設定については前回行っているので、今回は早速実装 & 必要な設定をしていく。
TL;DR.
実装
FirebaseでGoogleログインできるようにする
WebUIから設定をする。
手順は下記の通り。
左カラムのAuthenticationをクリック
→デフォルトでUsersタブにいると思うので、そのまま表示されているログイン方法を設定
ボタンクリック
→一覧の中からGoogleを選択
→右上のトグルを有効に切り替える
→プロジェクトのサポートメール
にメールアドレスを入力して保存をクリック
ログイン処理実装
公式ドキュメントを参考に実装する。
const signInWithPopup = () => { // Googleプロバイダオブジェクトのインスタンスを作成 const googleAuthProvider = new firebase.auth.GoogleAuthProvider(); // 別タブでログイン画面に飛ばしたいため、signInWithPopupを使う // リダイレクトでログイン画面に飛ばしたい場合はsignInWithRedirectを使う return firebase.auth().signInWithPopup(googleAuthProvider); }
ログアウト処理実装
const signOut = () => { // signOutを呼び出すだけでOK return firebase.auth().signOut(); }
ログイン、ログアウトボタン実装
ログイン、ログアウトは↑の処理で良いが使いやすい用にボタンにする。
合わせてログイン/ログアウトの判定をするため、ログインしているかどうかを確認する処理も追加する。
// ログインしているかチェックするカスタムフックを作る const useFirebaseLogin = () => { // stateでログイン状態を保持 const [loggedin, setLoggedin] = useState(false); useEffect(() => { // 現在ログインしているユーザを取得 firebase.auth().onAuthStateChanged(user => { // ユーザ情報が取れればログイン状態 setLoggedin(!!user); }); }, []) // ログイン情報を返却 return loggedin; }; // ログイン、ログアウトボタンを作る export const FirebaseAuthComponent: React.FC = () => { const loggedin = useFirebaseLogin(); if (!loggedin) { // ログインしていなければログインボタンを表示 return <button onClick={() => signInWithPopup()}>ログイン</button>; } // ログインしているならログアウトボタンを表示 return <button onClick={() => signOut()}>ログアウト</button>; }
登録処理作成
Realtime Databaseに登録するための処理を作成する。
登録にはfirebase.database.Reference.set()
を使えば良い。
ただしこの登録はsetに渡した値での登録となる。
つまり、既存のデータも含めて渡してあげないと登録済みのデータが消える。
const useSetDocument = (ref: firebase.database.Reference) => { const updateDocument = useCallback( (document: unknown) => { // refについては前回の記事参照 // setに登録したいデータを渡してあげれば登録できる ref.set(document); }, [ref] ); return updateDocument; }; export const useRegisterData = () => { // 前回作ったuseDatabase()を使いref取得 const ref = useDatabase(''); const setDocument = useSetDocument(ref); // 登録済みのデータを全部取得する const {data: registeredData} = useFetchAllData(); // データを登録する関数を返却する const registerData = useCallback((registerData: { [key: string]: string }) => { // 既存のデータと登録するkey-valueを合わせて登録関数に渡す setDocument({...registeredData, ...registerData}); }, [setDocument, registeredData]); return registerData; }; // 参考:前回作ったuseDatabase const useDatabase = () => { return useMemo(() => firebase.database().ref('/sample'), []); }; const useFetchData = (ref: firebase.database.Reference) => { const [data, setData] = useState<{ [key: string]: string }>(); useEffect(() => { ref.on('value', snapshot => { if (snapshot && snapshot.val()) { setData(snapshot.val()); } }); return () => { ref.off(); }; }, [ref]); return { data }; }
登録フォーム作成
key-valueを登録するフォームを作成する。
今回は登録できることだけを目的として一旦必要な諸々は見なかったことにする。
export const FormComponent: React.FC = () => { // データを登録する関数 // ↑で作成したuseRegisterDataを使う const registerData = useRegisterData(); const [keyData, setKeyData] = useState<string>(''); const [valueData, setValueData] = useState<string>(''); return <> {/* 今回は登録できれば良いとして、keyとvalueについてそれぞれ登録するinputフォームを作る */} <label>Key: <input placeholder="key" onChange={(event: ChangeEvent<HTMLInputElement>) => setKeyData(event.target.value)}/></label> <label>Value: <input placeholder="value" onChange={(event: ChangeEvent<HTMLInputElement>) => setValueData(event.target.value)}/></label> {/* 登録ボタンを押したときにsetDocument関数を呼び出してデータを追加する */} <button onClick={() => setDocument({[keyData]: valueData})}>登録</button> </>; }
App.tsxを修正
前回、何もしないでListComponentを呼び出すだけにしてあったApp.tsx
を修正する。
修正はfirebaseへのログイン周りと登録フォームを新たに追加するのみ。
const App: React.FC = () => { return ( <> <FirebaseAuthComponent /> <ListComponent /> <FormComponent /> </> ); }
動作確認
ログイン後、keyとvalueのフォームに値を入れ登録ボタンを押す。
無事、フォーム上のリスト部分に値が表示されればOK。
おまけ
部分更新したい場合
更新にはfirebase.database.Reference.update()
を使う。
setを使うとfirebase.database().ref('/sample')
で指定したパス以下全てが更新されてしまうが、updateを使うと指定したパス以下でkeyが一致するオブジェクトをのみを変更できる。
{"key1": "value1", "key2": "value2"}
というデータが登録されている状態で、update({key1: 'value2'})
というデータを渡してupdateを呼び出すと{"key1": "value2", "key2", "value2"}
というように部分的に更新ができる。
一致するキーが無ければ新規登録になる。
(なので、1件ずつ登録する今回のサンプルではsetではなく、updateを使っても変わらない)
setの時と同じく関数を作っていく。
const useUpdateDocument = (ref: firebase.database.Reference) => { // ref.updateがObjectを受け取るので、Objectを引数に取る関数を定義 const updateDocument = useCallback((document: Object) => ref.update(document), [ref]); return updateDocument; } export const useUpdateData = () => { // setの時と同じくrefを取得して、 const ref = useDatabase(); // 関数呼び出して const updateDocument = useUpdateDocument(ref); // 更新処理を作成する const updateData = useCallback((registerData: {[key: string]: string}) => { updateDocument(registerData); }, [updateDocument]); return updateData; }
登録フォームのComponentに更新ボタンを追加する。
export const FormComponent: React.FC = () => { const registerData = useRegisterData(); // 更新処理を呼び出す const updateData = useUpdateData(); const [keyData, setKeyData] = useState<string>(''); const [valueData, setValueData] = useState<string>(''); return <> <label>Key: <input placeholder="key" onChange={(event: ChangeEvent<HTMLInputElement>) => setKeyData(event.target.value)}/></label> <label>Value: <input placeholder="value" onChange={(event: ChangeEvent<HTMLInputElement>) => setValueData(event.target.value)}/></label> <button onClick={() => registerData({[keyData]: valueData})}>登録</button> {/* ボタン押したときに対象のデータを更新 */} <button onClick={() => updateData({[keyData]: valueData})}>更新</button> </>; }
削除したい場合
削除にはfirebase.database.Reference.remove()
を使う。
remove()を使うとfirebase.database().ref('/sample')
で指定したパス以下のデータ全てが削除される。
こちらも同じく関数を作っていく。
const useRemoveDocument = (ref: firebase.database.Reference) => { // 特に引数が必要ないのでただ呼び出すのみ const deleteDocument = useCallback(() => ref.remove(), [ref]); return deleteDocument; } // set、updateと同じなので割愛 export const useDelteData = () => { const ref = useDatabase(); const removeDocument = useRemoveDocument(ref); const deleteData = useCallback(() => removeDocument(), [removeDocument]) return deleteData; }
同じく登録フォームのComponentに削除ボタンを追加する。
export const FormComponent: React.FC = () => { const registerData = useRegisterData(); const updateData = useUpdateData(); // 削除処理を呼び出す const deleteData = useDelteData(); const [keyData, setKeyData] = useState<string>(''); const [valueData, setValueData] = useState<string>(''); return <> <label>Key: <input placeholder="key" onChange={(event: ChangeEvent<HTMLInputElement>) => setKeyData(event.target.value)}/></label> <label>Value: <input placeholder="value" onChange={(event: ChangeEvent<HTMLInputElement>) => setValueData(event.target.value)}/></label> <button onClick={() => registerData({[keyData]: valueData})}>登録</button> <button onClick={() => updateData({[keyData]: valueData})}>更新</button> {/* ボタン押したときに対象のデータを全消し */} <button onClick={() => deleteData()}>全消し</button> </>; }
まとめ
前回積み残していた更新分をまとめた。
これで一通りデータの読み書きができるので今度は実際になにか作ってみたいところ。
参考リンク
Firebase Realtime DatabaseをReactで使ってみる〜読み込み編〜
はじめに
Firebase Realtime DatabaseをReact使ってみた今日このごろ。
備忘録も兼ねて使い方をまとめておく。
まとめていたら思ったより長くなったので、今回は読み込みまでやる。
今回はReact(クライアント側)の設定だけで、Firebaseのプロジェクトの設定などは割愛する。
※一応やったことはWebUIポチポチしただけ。
TL;DR.
Reactプロジェクト作成
create-react-app
からスタートする。
サクッと用意。
$ npx create-react-app realtime-db-react --template typescript
Firebase周りの設定
プロジェクトの準備
FirebaseCLIを使って設定を進める。
設定は公式のリファレンスを見ながら行う。
また、プロジェクトは作ってある前提とする。
# ログインしていなければログインする $ npx firebase login # Firebaseプロジェクト初期化 $ npx firebase init # 設定した内容は下記の通り >? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. ❯◉ Database: Deploy Firebase Realtime Database Rules ? Please select an option: ❯ Use an existing project ? Select a default Firebase project for this directory: ❯ 作成しておいたプロジェクト # デフォルトのまま ? What file should be used for Database Rules? database.rules.json ✔ Firebase initialization complete!
DBのルール変更
上記手順で作ったままではDBに読み書きできないようになっているので少し修正する。
生成されているdatabase.rules.json
を下記のように修正。
{ "rules": { // 読むのは誰でも可能 ".read": true, // 書くのは認証済みの場合のみ ".write": "auth != null" } }
ルール反映
json変えたではもちろん反映されないので、firebase deploy
コマンドを叩いて反映する。
反映できているかどうかは下記手順で確認できる。
表示されるURLからコンソールを表示
→左カラムにあるDatabase
を選択
→メインカラム上部のプルダウンからRealtime Database
を選択
→ルールタブを表示
$ npx firebase deploy # ↓が表示されればOK ✔ Deploy complete! Project Console: https://console.firebase.google.com/project/hogehoge/overview
テスト用データ追加
確認用にWebUIからデータ追加しておく。
追加したのは↓の感じ。
{ "sample": { "key1": "value1", "key2": "value2" } }
実装
Firebase SDKとFirebase Admin SDKのインストール
アプリからFirebaseを使うために必要なライブラリをインストールする。
adminの方は認証がいる場合に必要なのでどちらも入れておく。
$ yarn add firebase firebase-admin
初期化周り
初期化に必要な情報をWebUIから取ってくる。
プロジェクトのページ左カラムにある歯車をクリック
→プロジェクトの設定をクリック
→Settingsページ、全般タブ下側にあるマイアプリにある</>
マークをクリック
→ウェブアプリにFirebaseを追加のページに飛ぶので、お好みの名前を入れて登録をクリック(hostingはお好みで。今回はやらない)
→スクリプトが表示されるので、firebaseConfig
の内容だけコピーする
必要な情報を取れたのでコードを書いていく。
初期化はこれでOK
import firebase from 'firebase/app'; // 認証周りやDB周りで必要なためimportしておく import 'firebase/auth'; import 'firebase/database'; // コピーしてきたfirebaseConfigそのまま // 元がvarで宣言されているので、constに変更 const firebaseConfig = { // コピペ }; firebase.initializeApp(firebaseConfig); export { firebase };
データベースに接続する
データのやり取りはfirebase.database.Reference
を通してやり取りされる。
今回は決まったパスのデータを取得したいので、パスを指定した上でメモ化しておく。
// カスタムフックにしておく const useDatabase = () => { // 同じパスでは毎回同じ結果が得られるのでmemo化しておく return useMemo(() => firebase.database().ref('/sample'), []); };
データを取得する
↑で作ったReferenceを受け取る関数を作成する。
Referenceのイベントをlistenすることでデータを取得できる。
指定したパスのデータに対する更新をすべて検知するにはvalue
を指定すれば良い。
// hooksを使いたいのでカスタムhooksにしておく const useFetchData = (ref: firebase.database.Reference) => { const [data, setData] = useState<{[key: string]: string}>(); useEffect(() => { // イベントリスナーを追加するにはonを使う ref.on('value', snapshot => { // パスに対する全データを含むsnapshotが渡される // ない場合はnullが変えるので存在をチェックしておく if (snapshot?.val()) { setData(snapshot.val()); } }); return () => { ref.off(); }; // refの変更に応じて再取得する }, [ref]); // データを返却する return { data }; } // 実際に呼び出す際はこちらを使う export const useFetchAllData = () => { // refを取得して const ref = useDatabase(); // ref渡してデータを取得する return useFetchData(ref); };
Componentからデータ取得する
useFetchAllData
を使ってデータを取ってくるコンポーネントを作成する。
取得したデータはobject形式なので、list形式に変換してから表示する。
import React, { useMemo } from 'react'; import { useFetchAllData } from '../firebase/firebaseDB'; export const ListComponent: React.FC = () => { // dataを取ってくる const { data } = useFetchAllData(); // object形式なので使いやすいように{key, value}形式のリストに変換する // また、データが変わらない限り結果は同じなのでメモ化しておく const dataList = useMemo(() => Object.entries(data || {}).map(([key, value]) => ({ key, value })), [data]); return <dl>{dataList.map(({ key, value }) => <React.Fragment key={`${key}${value}`}> <dt>key: {key}</dt> <dt>value: {value}</dt> </React.Fragment> )}</dl> };
起動した際に画像のようにデータを表示できればOK。
まとめ
今回はReactでFirebase Realtime Databaseを使ってデータの読み込みができるところまでやった。
最初にも書いたけど思ったより長くなったので今回はここまでにしておく。
参考サイト
FIREBASE FATAL ERROR: Can't determine Firebase Database URL.の対処法
はじめに
firebaseに接続しようとしてハマったので、トラブルシューティングメモ。
エラーメッセージ
Error: FIREBASE FATAL ERROR: Can't determine Firebase Database URL. Be sure to include databaseURL option when calling firebase.initializeApp().
エラーになるコード
import React from 'react'; import firebase from 'firebase/app'; import 'firebase/auth'; import 'firebase/database'; const config = { apiKey: process.env.REACT_APP_API_KEY, authDomain: process.env.REACT_APP_AUTH_DOMAIN, projectId: process.env.REACT_APP_PROJECT_ID, databaseUrl: process.env.REACT_APP_DATABASE_URL }; firebase.initializeApp(config);
REACT_APP_API_KEY=xxxxxx REACT_APP_AUTH_DOMAIN=xxxxx REACT_APP_PROJECT_ID=xxxxxx REACT_APP_DATABASE_URL=xxxxx
対応内容
WebUIからwebアプリ用の設定ファイルを入手。
見比べて気づいたけど、config
のキー名が間違っていた。
const config = { apiKey: process.env.REACT_APP_API_KEY, authDomain: process.env.REACT_APP_AUTH_DOMAIN, projectId: process.env.REACT_APP_PROJECT_ID, // URLは大文字 databaseURL: process.env.REACT_APP_DATABASE_URL }; firebase.initializeApp(config);
よくよく見たらBe sure to include databaseURL option
って書いてあるので、ちゃんとエラーメッセージ読めばよかった。。。
参考リンク
Angular+Electron事始め
はじめに
Electronが気になっていた今日このごろ。
Angularを久しぶりに触りたいこともあり、Angular + Electronでアプリを作ってみようと思う。
とりあえずAngular-CLIで作ったテンプレートをElectron使って起動できるところまですすめていく。
TL;DR.
手順
Angularプロジェクト作成
# いつも通りCLIからスタート $ng new angular-electron
Electronインストール
$ npm install electron --save-dev
起動できるように各種修正
公式ドキュメント)とangular-electron)を参考に作っていく。
Angular.jsonを修正
projects > architect > build > oprions > outputPath
を/dist/プロジェクト名(今回はangular-electron)
をdist
のみに修正。
{ ... "projects": { ... "architect": { "build": { ... "options": { "outputPath": "dist", ...
main.jsを作成
アプリを起動するためのjsを作成する。
tsで書きたかったりホットリロードとかも試したいところだけど、
一旦jsでちゃんと動くところまで確認する。
const { app, BrowserWindow, screen } = require('electron'); const url = require('url'); const path = require('path'); let win = null; function createWindow() { const electronScreen = screen; const size = electronScreen.getPrimaryDisplay().workAreaSize; // ブラウザウインドウを作成 win = new BrowserWindow({ x: 0, y: 0, width: size.width, height: size.height, webPreferences: { nodeIntegration: true } }) win.loadURL(url.format({ pathname: path.join(__dirname, 'dist/index.html'), protocol: 'file:', slashes: true })); // dev toolを開く win.webContents.openDevTools(); win.on('closed', () => win = null); return win; }; app.allowRendererProcessReuse = true; app.on('ready', () => setTimeout(createWindow, 400)); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { if (win === null) { createWindow(); } });
package.jsonを修正
electron
コマンドで起動できるようにpackage.json
のmain
に上記ファイルを指定する。
{ "name": "angular-electron", "main": "main.js", ... }
起動できることを確認する
アプリを起動できることを確認する。
その前にAngularのビルドが必要なので実行しておく。
# Angularのファイルをビルド(最初から入っているコマンドそのまま) $ npm run build > dist配下にファイルが出力されていることを確認 # アプリを起動してみる $ npx electron .
画像のようにAngular-CLIのテンプレートが表示されることを確認できればOK。
まとめ
Electron + Angularでアプリを作ってみようと思ったので、とりあえず起動できるまでやった。
起動だけならさっくりできたので、次はmain.js
をts化したり、アプリを作っていきたいところ。
参考サイト
ReactHooksざっくりまとめ
はじめに
Hooksを触り始めている今日この頃。
雰囲気で色々触っていたけど、だんだん辛くなってきたのでちゃんとまとめていこうと思う。
APIリファレンスを見ながら、簡単に使い方とサンプルをまとめておく。
TL;DR.
useState
component内で使うステートフルな値と、それを更新するための関数を返す。
Class Componentのstateと同じ。
使い方
import { useState } from 'react'; // useStateに渡した値がstateの初期値になる const [state, setState] = useState(initialState); // stateの値を更新したいときはsetStateを使う setState(updateState); // 型指定したい場合、useStateの際に指定できる const [stringState, setStringState] = useState<string>(initialState); // 初期化を遅延させることもできる const [lagyState, setLagyState] = useState(() => { // 初期値をなにかしら受け取る const initialState = getHoge(); return initialState; });
サンプル
import React, { useState } from 'react'; export const StateComponent: React.FC<any> = () => { // useStateを使う const [greetingMessage, setGreetingMessage] = useState<string>('hello'); return ( <> {/* stateの内容を表示 */} <div>{greetingMessage}</div> <form> {/* stateの内容を更新 */} <input type="text" onChange={text => setGreetingMessage(text.target.value)} /> </form> </> ) };
useEffect
副作用を有する可能性のある命令型コードを受け付ける。
ClassComponentで言うところのcomponentDidMount
やcomponentDidUpdate
の置き換え。
って言われているけど実際には近しい処理くらいが正しい気がする。
※componentDidMount
やcomponentDidUpdate
とは別物の認識。
使い方
// これだとレンダリングの度に毎回実行される useEffect(() => { // 副作用がある処理を行う const subscription = props.source.subscribe(); // クリーンアップ処理を行いたい場合は、useEffectに渡す関数の戻り値に関数を渡せば良い return () => { subscription.unsubscribe(); }; }); // 副作用が依存している値を第2配列に指定する // props.sourceに変更があったときのみ実行する useEffect(() => { const subscription = props.source.subscribe(); return () => { subscription.unsubscribe(); }; }, [props.source]); // 一度だけ実行したい場合△ useEffect(() => { const subscription = props.source.subscribe(); return () => { subscription.unsubscribe(); }; // 第2引数に空配列を渡してあげれば良い // 空配列を渡すことで、何にも依存しない(2回目が実行されることない)ことを伝える // 処理としてはprops.sourceに依存するため、渡して上げるのが良い }, []); // 一度だけ実行したい場合◎ useEffect(() => { function doSomething() { console.log(someProp); } doSomething(); }, [someProp]);
サンプル
export const EffectComponent: React.FC<any> = () => { const [count, setCount] = useState(0); // useEffectを使う useEffect(() => { const interval = setInterval(() => setCount(count + 1), 1000); // intervalをリセットする return () => clearInterval(interval); // このeffectはcountに依存しているので、countを第二引数に渡す // →ここを渡さないとeffect無いではstateが初期値から変わらない }, [count]); return <div>{count}</div> };
useContext
コンテクストオブジェクトを受け取り、そのコンテクストの現在値を返す。
コンテクストの現在値は、ツリー内でこのフックを呼んだコンポーネントの直近にある <MyContext.Provider>
の value の値によって決定される。
ざっくり孫コンポーネントなど階層が離れているコンポーネントに値を渡せるようになる認識。
(バケツリレーをしなくても良くなる)
いつ使うのが良いかは公式ドキュメントを読むのが良さそう。
使い方
const themes = { light: { foreground: "#000000", background: "#eeeeee" }, dark: { foreground: "#ffffff", background: "#222222" } }; const ThemeContext = React.createContext(themes.light); // 型を指定したければ下記のようにする type ThemeColor = { foreground: string; background: string; } const ThemeContext = React.createContext<ThemeColor>(themes.light); export const App = () => ( // ここでcontextの現在値がdarkになる <ThemeContext.Provider value={themes.dark}> <Toolbar /> </ThemeContext.Provider> ); export const AppInitial = () => ( // 何も渡さなければ初期値(themes.light)になる <Toolbar /> ); const Toolbar = props => ( <div> <ThemedButton /> </div> ); const ThemedButton = () => { const theme = useContext(ThemeContext); return ( <button style={{ background: theme.background, color: theme.foreground }}> I am styled by theme context! </button> ); }
サンプル
import React, {useContext} from 'react'; // Contextを作成 const MemoContext = React.createContext<string>('initial text.'); export const ContextComponent = () => ( // textの内容を変える <MemoContext.Provider value="memo text."> <MemoArea /> </MemoContext.Provider> ); const MemoArea = () => ( <div> <Text /> </div> ); const Text = () => { const theme = useContext(MemoContext); return ( <span>{theme}</span> ); }
useReducer
useState
の親戚。Reduxのreducerを記述できる。
(state, action) => newState
という型のreducer を受け取り、現在のstate
をdispatch
メソッドとペアにして返す。
使い方
// initialState const intialState = { count: 0 } // Reducerを作成(stateとactionを受け取る関数) const reducer = (state, action) => { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } } // useRedcerは第一引数にreducer、第二引数にinitialStateを渡してあげる // stateとdispatchメソッドが返ってくる(どちらもReduxの使い方とほぼ同じ) const [state, dispatch] = useReducer(reducer, initialState); // 初期化を遅延させたい場合や特定の場合に初期値にさせたい場合、 // 第三引数に初期化関数を渡してあげる const init(initialCount) => ({count: initialCount}); const reducer = (state, action) => { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; case 'reset': return init(action.payload); default: throw new Error(); } } const Counter = ({initialCount}) => { const [state, dispatch] = useReducer(reducer, initialCount, init); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset </button> </> ); }
サンプル
import React, {useReducer} from 'react'; type State = {count: number}; const initialState: State = {count: 0}; const add = (num: number) => ({type: 'ADD', payload: num}); const subtract = (num: number) => ({type: 'SUBTRACT', payload: num}); type Action = ReturnType<typeof add | typeof subtract>; const reducer = (state: State, action: Action) => { switch (action.type) { case 'ADD': return {count: state.count + action.payload}; case 'SUBTRACT': return {count: state.count - action.payload}; default: throw new Error(); } } export const ReducerComponent = () => { const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch(subtract(1))}>-</button> <button onClick={() => dispatch(add(1))}>+</button> </> ); }
useCallback
メモ化されたコールバックを返す。
第一引数にコールバック関数、第二引数にコールバックが依存している値を配列で渡す。
使い方
const memoizedCallback = useCallback( // 第一引数に関数を指定 () => doSomething(a, b), // 第一引数に渡した関数が依存している値(a, b)をリストで渡してあげる [a, b], );
サンプル
import React, {useCallback, useState} from 'react'; export const CallbackComponent = () => { const [count, setCount] = useState(0); // コールバックはcountに依存しているので、第二引数にcountを渡してあげる // 渡さないと関数内のcountの値が更新されない const buttonClick = useCallback(() => setCount(count + 1), [count]); return <ButtonComponent count={count} buttonClick={buttonClick}/> } const ButtonComponent = ({count, buttonClick}: {count: number, buttonClick: () => void}) => ( <div> count: {count} <button onClick={buttonClick}>click</button> </div> )
useMemo
メモ化された値を返す。
関数の結果を保持することができるので、同じ引数で関数を呼び出した時には中身の処理は実行せず結果だけ返す。
これによりレンダリングの度に重い処理が走らなくて済むようになる。
使い方
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
サンプル
あまり良い例を思いつかなかった。
import React, { useMemo } from 'react'; export const MemoComponent = () => { const count = 10000; // 10000回足し算行う const memoValue = useMemo(() => [...Array(count)].reduce((pure) => pure+1, 0), [count]); return <div>{memoValue}</div> }
useRef
.current
プロパティがuseRef
を呼び出した渡す値で初期化されたミュータブルなオブジェクトを返す。
返されるオブジェクトはコンポーネントが存在する限り、存在し続ける。
多分よく使われるのは、DOMにアクセスする場合にref={hogeRef}
みたいな使い方だと思う。
useRef
はref属性を扱うだけではなく、あらゆる書き換え可能な値を保持して多くためにも使える。
使い方
const refContainer = useRef(initialValue);
サンプル
import React, { useRef, RefObject } from 'react'; export const RefComponent: React.FC = () => { // Dom触るためのrefを作成 const useRefWithDom: RefObject<HTMLInputElement> = useRef<HTMLInputElement>(null); // 値を保持しておくrefを作成 const useRefWithValue = useRef('initial value'); return ( <div> <input type="text" ref={useRefWithDom} /> <button onClick={() => { if (!useRefWithDom.current) { return; } // 変更前('initial value') console.log('useRefWithValue.current: ', useRefWithValue.current); // currentを上書きすることで変更できる useRefWithValue.current = useRefWithDom.current.value; // 変更後('hoge') console.log('useRefWithValue.current: ', useRefWithValue.current); }}>ボタン</button> </div> ); };
useImperativeHandle
ref
が使われた時に親コンポーネントに渡されるインスタンス値をカスタマイズするのに使う。
使う時にはforwardRef
と一緒に使う。
使い方
function FancyInput(props, ref) { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); } })); return <input ref={inputRef} ... />; } FancyInput = forwardRef(FancyInput);
サンプル
import React, { forwardRef, useRef, useImperativeHandle, RefObject } from 'react'; const FancyInput = (props: any, ref: any) => { // refを定義 const inputRef: RefObject<HTMLInputElement> = useRef<HTMLInputElement>(null); // 受け取ったrefにfocus関数を追加する useImperativeHandle(ref, () => ({ focus: () => inputRef?.current?.focus() })); return <input ref={inputRef} />; } // forwardRefに作った関数を渡す(変数に置かなくても問題なし) const FancyInputRef = forwardRef(FancyInput); export const ImperativeHandleComponent: React.FC = () => { // ここで使うrefを定義 const ref: RefObject<HTMLInputElement> = useRef<HTMLInputElement>(null); return (<> {/* refを渡す(このrefにfocus関数が追加される) */} <FancyInputRef ref={ref} /> {/* クリックした時にテキストボックスにフォーカスする */} <button onClick={() => ref?.current?.focus()}>clickでフォーカス</button> </>) };
useLayoutEffect
基本的にはuseEffect
と同じ。
違う点はDOM の変更があった後で同期的に副作用が呼び出されるところ。
DOMを操作して再描画する場合に使う。
最初はuseEffect
を使って、問題があるときのみuseLayoutEffect
を使う方が良い。
と、公式サイトに書いてある。
useDebugValue
React DevTools でカスタムフックのラベルを表示することができる。
使い方
useDebugValue(value)
サンプル
const useFriendStatus = (friendID) => { const [isOnline, setIsOnline] = useState(null); // DevToolのラベルが下記のように表示される // "FriendStatus: Online" useDebugValue(isOnline ? 'Online' : 'Offline'); return isOnline; }
まとめ
Hooksについてざっくりまとめた。
useState
とかuseEffect
とか基本的なHooksについてはだいぶわかりやすいと思った。
メモ化ができるuseMemo
とuseCallback
については常に使えば良いってわけじゃなさそうなので、使っていくうちに使い所を見極めたい。
参考サイト
GatsbyJSでGithubPagesに静的サイトを作る
はじめに
Angularで静的サイトを作ってた今日このごろ。
Gatsbyも触ってみたかったので今回はGatsbyを使って静的サイトをGithubPagesに公開してみる。
ただ公開するのはチュートリアルですぐできそうなので、
Markdownページを公開できるようにするのと自動デプロイも試してみる。
TL;DR.
導入
公式のQuickStartを見ながら進めていく。
$ npx gatsby-cli new gatsby-blog > yarnとnpmどっち使うか聞かれるので好みで設定
yarn (npm run) develop
で↓のようなページが表示されれば導入は完了。
TypeScript化する
デフォではjsなのでts対応する。
公式からプラグインが出ているのでそれを使うことにする。
必要なものをインストール
# プラグインとtsをインストール $ yarn add gatsby-plugin-typescript typescript # 型定義もインストールしておく $ yarn add --dev @types/react @types/react-dom @types/node
tsconfigの作成
サクッと設定。
ミニマムではないと思うけど、問題ないと思うのでこれでやる。
{ "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react" }, "include": [ "./src/**/*" ] }
ts化
ファイル全般を.tsx
に変更すれば完了
Markdown対応
静的サイトはこれまでの手順で作成可能だが一々HTMLを書くのはしんどいので、
Markdownで書いたものを表示できるようにする。
Markdownに対応させるのはlibraryがあるのでそれを使う。
導入方法は公式に乗っているので参考にしながらすすめる。
libraryのインストール
$ yarn add gatsby-source-filesystem gatsby-transformer-remark
Markdownファイルを作成
適当にMarkdownの記法を色々試してみる。
—--
で囲まれた部分はコンテンツではなく、メタデータとして扱われる。
--- path: "/blog/20200318" date: "2020-03-18" title: "My first blog post" tag: ["JavaScript", "TypeScript"] --- # はじめに マークダウンを読めるようになったので試す。 # TL;DR. このページ # ヘディング ## レベル2 はこんなもん ### レベル3 はこれ #### レベル4 だよ # リスト * hoge * huga * piyo # 順番付き 1. first 2. second 3. third # コード系 インライン`code`はこんな感じ ブロックは↓みたいな
const hoge = () => 'hoge'; console.log(hoge());
# テーブル |title|value| |:---:|:----| |中央寄せの文字を入れている。|左寄せの値を入れてみている。| |2段目タイトル|2段目バリュー| # 文字装飾 **ボールド** _イタリック_ # 引用 > 引用してみた >> もっと
gatsby-configに追加
gatsby-source-filesystem
これでMarkdownをソースとして読み込むことができるようになる。
{ resolve: `gatsby-source-filesystem`, options: { name: `markdown-pages`, path: `${__dirname}/src/markdown-pages` } }
gatsby-transformer-remark
これを使用することでMarkdownのコンテンツ部分をHTMLに。
メタデータ部分をformatter
に変換してくれる。
{ resolve: `gatsby-source-filesystem`, options: { name: `markdown-pages`, path: `${__dirname}/src/markdown-pages` } }, // 追記 `gatsby-transformer-remark`
Markdownファイルのテンプレート作成
とりあえず公式のテンプレートそのまま使う。
(tsxに変更しただけ)
import React from "react" import { graphql } from "gatsby" export default function Template({ data }) { const { markdownRemark } = data // data.markdownRemark holds your post data const { frontmatter, html } = markdownRemark return ( <div className="blog-post-container"> <div className="blog-post"> <h1>{frontmatter.title}</h1> <h2>{frontmatter.date}</h2> <div className="blog-post-content" dangerouslySetInnerHTML={{ __html: html }} /> </div> </div> ) } export const pageQuery = graphql` query($path: String!) { markdownRemark(frontmatter: { path: { eq: $path } }) { html frontmatter { date(formatString: "MMMM DD, YYYY") path title } } } `
gatsby-nodeを使ってページを作成する
Markdownで書いたページから静的ページを作成するため、gatsby-node
を用意する。
今回はTypeScriptを使ったので、ts-node
を使う。
基本は公式のままで、ts-node
部分を足しただけ。
const path = require(`path`) // ここを追加 require('ts-node').register({ compilerOptions: { module: 'commonjs', target: 'esnext', }, }); exports.createPages = async ({ actions, graphql, reporter }) => { const { createPage } = actions const blogPostTemplate = path.resolve(`src/templates/blogTemplate.tsx`) const result = await graphql(` { allMarkdownRemark( sort: { order: DESC, fields: [frontmatter___date] } limit: 1000 ) { edges { node { frontmatter { path } } } } } `) // Handle errors if (result.errors) { reporter.panicOnBuild(`Error while running GraphQL query.`) return } result.data.allMarkdownRemark.edges.forEach(({ node }) => { createPage({ path: node.frontmatter.path, component: blogPostTemplate, context: {}, // additional data can be passed via context }) }) }
GithubActionsを使って自動デプロイをする
GithubPagesに公開するのは有効にした上で、Markdownで書いたものをbuildしてpushすれば完了する。
ただ、公開の都度buildするのはめんどくさいのでActionsを使って自動化してみる。
※GithubPagesの公開方法は公式参照
workflow作成
GithubPagesにデプロイするActionはすでにあるので、これを使ってやってみる。
ただし、このActionはデプロイ先のブランチをすべて上書きしてしまうっぽいので、
pagesを公開するブランチ以外をdefaultブランチにしたほうが良いかもしれない。
(自分はmasterブランチを公開用にして、作業用をdevelopブランチとした)
name: deploy-pages on: push: # 作業用ブランチにpushされた時に動かす branches: - develop jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: setup uses: actions/setup-node@v1 with: node-version: 12 - name: install and setup config run: npm install # Gatsbyのビルド - name: build run: npm run build # pagesへdeployする - name: deploy uses: peaceiris/actions-gh-pages@v3 with: personal_token: ${{ secrets.PERSONAL_TOKEN }} # developブランチのpublicディレクトリの内容を publish_dir: ./public # masterブランチにpushする publish_branch: master
まとめ
GatsbyJSを使って静的サイトを作った。
Gatsbyをts化してうまく動かすところに苦戦した。
今回の様な使い方をするのであれば、
型があるメリットも薄い気がしたのであまり必要ないかなと思った。
ブログ部分のレイアウトも自由にできるのでもっとマシにして行ければなと。
+Gatsbyが意外と色々できる気がしたのでこれからもちょくちょく触っていきたいところ。