水無瀬のプログラミング日記

プログラムの勉強した時のメモ

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

登録するデータを入力する箇所はリアクティブフォームで作ってみる。
リアクティブフォームについては昔メモしたのでそちらを参考に。

Angularのリアクティブフォームまとめ - 水無瀬のプログラミング日記

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。

f:id:minase_mira:20200711121838p:plain

次にhttp://localhost:4200/formにアクセスしてみる。
画像の様にログインボタン、登録フォーム、削除ボタンが表示されていればOK。

f:id:minase_mira:20200711121852p:plain

まとめ

今回は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。f:id:minase_mira:20200626211124p:plain

まとめ

今回は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.jsonmainに上記ファイルを指定する。

{
    "name": "angular-electron",
    "main": "main.js",
    ...
}

起動できることを確認する

アプリを起動できることを確認する。
その前にAngularのビルドが必要なので実行しておく。

# Angularのファイルをビルド(最初から入っているコマンドそのまま)
$ npm run build
> dist配下にファイルが出力されていることを確認

# アプリを起動してみる
$ npx electron .

画像のようにAngular-CLIのテンプレートが表示されることを確認できればOK。 f:id:minase_mira:20200609232413p:plain

まとめ

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で言うところのcomponentDidMountcomponentDidUpdateの置き換え。
って言われているけど実際には近しい処理くらいが正しい気がする。
componentDidMountcomponentDidUpdateとは別物の認識。

使い方

// これだとレンダリングの度に毎回実行される
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 を受け取り、現在のstatedispatchメソッドとペアにして返す。

使い方

// 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についてはだいぶわかりやすいと思った。
メモ化ができるuseMemouseCallbackについては常に使えば良いってわけじゃなさそうなので、使っていくうちに使い所を見極めたい。

参考サイト

GatsbyJSでGithubPagesに静的サイトを作る

はじめに

Angularで静的サイトを作ってた今日このごろ。
Gatsbyも触ってみたかったので今回はGatsbyを使って静的サイトをGithubPagesに公開してみる。

ただ公開するのはチュートリアルですぐできそうなので、
Markdownページを公開できるようにするのと自動デプロイも試してみる。

TL;DR.

今回作ったもの

導入

公式のQuickStartを見ながら進めていく。

$ npx gatsby-cli new gatsby-blog
> yarnとnpmどっち使うか聞かれるので好みで設定

yarn (npm run) developで↓のようなページが表示されれば導入は完了。 f:id:minase_mira:20200322194339p:plain

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が意外と色々できる気がしたのでこれからもちょくちょく触っていきたいところ。

参考リンク