Elm portsでFirebase Firestoreを触ろう!


ElmとFirebaseの記事が絶望的に少なかったので書いておくことにしました。特に認証が絡んだ記事がなかったので良ければ参考にしてください。

今回やったこと

今回行ったことは単純です。

Google認証をする -> 認証が成功すればinputが出現 -> Firestoreからデータを取得inputvalueとして埋める -> inputの値が変更されたときFirestoreの値(uidごと)を書き換える

JavaScriptとのやりとり: Ports

まず前提知識としてFirebaseのようにJavaScriptの基本機能ではなくライブラリでElmからその資産を使いたい場合には、Portsという仕組みを使います。Portsの基本的な使い方や概念については公式ドキュメントを読むと良いでしょう。

ドキュメントから大事な部分を抜き出しておきます。

Elm と JavaScript は、ポートを通じて互いに一方的に送信を行うことで、通信をすることができる

実装解説

Ports

Elm -> JavaScript と送信をするPortsです。

signInは、ボタンを押されたときに、Google認証を促す部分です。
pushは、inputの要素が書き換えられたときに、文字列をFirestoreに渡して格納する部分です。

port signIn : () -> Cmd msg 
port push : String -> Cmd msg

JavaScript -> Elm と待ち構えるPortsです。
readは、Firestoreの格納した値(文字列)を受け取ります。
signedInは、Google認証が成功したときにinputを出現させます。

port read : (String -> msg) -> Sub msg
port signedIn : (Bool -> msg) -> Sub msg

ports関数を持つmoduleは、port moduleと宣言し、JavaScript -> ElmとJavaScriptからアクションをする必要がある関数はexposing(公開)する必要があります。

port module Main exposing
    (...
    , read
    , signedIn
    , ...
    )

JavaScriptから待ち受ける

subscriptions関数から待ち構える必要があります。複数待ち構える場合は、Sub.batchにリスト形式で渡します。

type Msg
    = SignIn
    | Push String
    | Read String
    | SignedIn Bool

subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch [ signedIn SignedIn, read Read ]

updateでは受け取ってセットするだけです。

type alias Model =
    { pushText : String
    , isSignedIn : Bool
    }

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ...
        SignedIn isSignedIn ->
            ( { model | isSignedIn = isSignedIn }, Cmd.none )

        Read text ->
            ( { model | pushText = text }, Cmd.none )
        ...

-- Modelはinput要素の出し分けやinputの値に使われます。
view : Model -> Html Msg
view { pushText, isSignedIn } =
    div [ class "container" ]
        [ button [ onClick SignIn ] [ text "Google サインイン" ]
        , if isSignedIn then
            input [ type_ "input", value pushText, onInput Push ] []

          else
            text ""
        ]

ElmからJavaScriptにメッセージを送る

先程宣言したports関数を呼び出すだけです。値を必ず渡さないといけないため、signInは空のタプル(Unit)を渡しています。

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ...
        SignIn ->
            ( model, signIn () )

        Push text ->
            ( { model | pushText = text }, push text )
        ...

ElmとFirebaseの初期設定

こちらがFirebaseとElmの初期設定になります。app.と書かれていたらElmに関する呼び出し。firebase.と書かれていたらFirebaseに関わる呼び出し(認証等)。DB.と書かれていたらFirestoreに関する呼び出しであることを心に留めておいてください。

const app = Elm.Main.init();

const config = {
  /* firebase config */
};
firebase.initializeApp(config);
const provider = new firebase.auth.GoogleAuthProvider();
const DB = firebase.firestore();

Elmから待ち構える

Elmから値を受け取りましょう。とても簡単です。app.ports.[定義したports関数].subscribe([Elmから投げられた値] => {}); という形になっているだけです。

signInは、Google認証をします。
pushは、Firestoreに値を入れ込みます。

// ログイン監視
app.ports.signIn.subscribe(_ => {
  firebase.auth().signInWithPopup(provider).then((_) => {}).catch((error) => {});
});

app.ports.push.subscribe(text => {
  const user = firebase.auth().currentUser;
  DB.collection('foo').doc(user.uid).set({input: text});
});

JavaScriptからElmにメッセージを送る

またまた簡単です。app.ports.[portsで定義した関数].send([Elmに渡したい値])となります。説明不要ですね。どちらかと言うとFirebaseの知識が肝です。onAuthStateChangedでサインインが成功(userがある)したときに、FirestoreのonSnapshotのコールバックでsendするのがポイントです。

// ログイン監視
firebase.auth().onAuthStateChanged((user) => {
  if (user) {
    app.ports.signedIn.send(true);

    // ログイン後にDatabase監視
    DB.collection('foo').doc(user.uid).onSnapshot((doc) => {
        const data = doc.data();
        app.ports.read.send(data.input);
    });
  }
});

ソースコード全体

こちらにソースコードがあります。通してみてイメージを膨らませてください。

まとめ

ElmとJavaScriptが通信するにはメッセージを一方的に送信しあうことがポイントです。そのイメージが掴めればFirebaseでも他のJavaScriptライブラリでも要領は同じです。Elm側は型安全や記述のシンプルさを活かしつつ、JavaScriptにしか出来ない仕事をドンドン押し付けてリッチなアプリケーションを作ってみましょう!