DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Build a complete realtime poll app with ionic, react, firebase and typescript (Part 2)
Ralf πŸ’₯πŸ”₯β˜„οΈ
Ralf πŸ’₯πŸ”₯β˜„οΈ

Posted on • Originally published at gymconsole.app

Build a complete realtime poll app with ionic, react, firebase and typescript (Part 2)

Building a realtime polling app with ionic, react and firebase Part 2

In the last part (https://gymconsole.app/blog/ionic-firebase-poll-app) we mainly built the UI for our app with react and ionic. We also mocked the poll and
answer objects.
In this part we are finally going to add firebase and make the app fully functional.

The full source code of the application can be found here:
https://github.com/RalliPi/realtime-poll-app

And here is what our app will look like when we are done:
Poll app demo video

Before we can start coding, we need to set up a firebase project.

Head over to https://console.firebase.google.com and create a new project. Fill in all the required fields and wait till
your project got created.
Next, click on 'Cloud Firestore' and create a new cloud firestore database.
When the database got created, you will see something like that:

Pic of empty firestore

Let's add in our first poll. This article will not explain how to add new polls programmatically. We will enter the poll
by hand and users can vote for answers with our app. (Actually creating polls from our app could be another article in
the future).

Click 'Start collection' and enter 'polls' as collection id. This will be the collection where we store every single
poll the users can vote for. Click next.

Let's add in our first poll. Use 'poll1' as document id and add a field called text of type string.

Pic of poll setup

Now we can add the possible answers to our poll. We will be using a subcollection here. A subcollection is basically a
collection inside of a single document. We will be using this feature to save as many answers to a poll as we want.
Every document in a subcollection belongs to its parent document.

Click 'Start collection' inside the newly created poll document. Call the subcollection 'answers' and create a new
document in it. The document should have a field called 'amount' (we will be using it to store the actual vote amount
the answer received) and a field called 'text' which is the text that we display in our app.

Pic of answers subcollection

Now let's copy the connection settings of the firebase app. We will need it later. Go to your project settings by
clicking the small gear in the upper left corner. Scroll down and copy the config of your firebase web-app.
It will look something like that:

const firebaseConfig = {
  apiKey: "yourapikey",
  authDomain: "yourauthdomain",
  databaseURL: "yourdatabaseurl",
  projectId: "yourprojectid",
  storageBucket: "yourstoragebucket",
  messagingSenderId: "yourmessagingsenderid",
  appId: "yourappid"
};

Warning:
This tutorial assumes that you have no security rules setup on your firestore database. That means everybody can read and write to your database. This is highly dangerous and should not get deployed to production.
But this guide is not about security rules. So I will not discuss them here.

Ok, that's it on the firebase side. It's time to get coding. Open up the project of part 1 in your editor.

Before we can actually start writing code, we need to install a dependency first. Open your terminal, change to your app
directory and run the following command:

npm install firebase

This will add the firebase sdk to our project. We need it in order to communicate with the firestore database.

We will firstly configure the firestore database. In your src folder, create a new file called db.ts. It will hold our
firebase configuration.
Paste the following code:

import * as firebase from "firebase/app";
import "firebase/firestore";

var firebaseApp = firebase.initializeApp({
  apiKey: "yourapikey",
  authDomain: "yourauthdomain",
  databaseURL: "yourdatabaseurl",
  projectId: "yourprojectid",
  storageBucket: "yourstoragebucket",
  messagingSenderId: "yourmessagingsenderid",
  appId: "yourappid",
});

export const db = firebaseApp.firestore();

First we import firebase and firestore. Afterwards we're initializing firebase with the configuration we copied earlier. In
the last line we export the firestore object and call it db. This way we can access firestore easily from every file we
import db.ts.

Create hooks to easily access polls and answers

Remember the poll and answer objects we used directly in our page. We are going to swap them out with two hooks. Those
hooks will be responsible for loading and saving polls and poll answers.

Go to your page component and swap the content with the following:

import {
  IonContent,
  IonHeader,
  IonPage,
  IonTitle,
  IonToolbar,
  IonCard,
  IonCardContent,
  IonList,
  IonItem,
  IonLabel,
} from "@ionic/react";
import React, { useState, useEffect } from "react";
import "./Home.css";
import { usePoll, usePollAnswers } from "../hooks/poll";

const Home: React.FC = () => {
  var poll = usePoll("poll1");
  var { answers, vote } = usePollAnswers("poll1");

  const onVote = (
    e: React.MouseEvent<HTMLIonItemElement, MouseEvent>,
    id: string
  ) => {
    e.preventDefault();
    let answer = answers.find((a) => a.id === id);
    vote(answer!.id);
  };

  const answerList = () => {
    return answers.map((answer) => (
      <IonItem onClick={(e) => onVote(e, answer.id)} key={answer.id}>
        <IonLabel>{answer.text}</IonLabel>
        <IonLabel>{answer.amount}</IonLabel>
      </IonItem>
    ));
  };

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonTitle>Ionic Blanks</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent>
        <IonCard>
          <IonCardContent>
            {poll != null ? poll.text : "loading poll..."}
          </IonCardContent>
        </IonCard>
        <IonList>{answerList()}</IonList>
      </IonContent>
    </IonPage>
  );
};

export default Home;

The component almost looks identical to the old version. We only swaped the useState hooks with two custom hooks caled
usePoll and usePollAnswers. We pass the id of the poll we want to use and the hooks handle the rest.
usePoll just returns the poll object from the firestore database and usePollAnswers returns a list of answers that
belong to a poll and additionally a method called vote which can be used to vote for a poll answer.

Let's get to work and implement these hooks:

Create a new directory in your src dir called hooks and create a .ts file called poll.ts in it.

Put the following content in:

import { useState, useEffect } from "react";
import { db } from "../db";
import { Poll } from "../model/poll";
import { PollAnswer } from "../model/pollAnswer";
import { firestore } from "firebase";

export const usePoll = (pollId: string) => {
  const [poll, setPoll] = useState<Poll | null>(null);

  useEffect(() => {
    //load current poll
    db.collection("polls")
      .doc(pollId)
      .get()
      .then((poll: firestore.DocumentSnapshot<firestore.DocumentData>) => {
        if (poll.exists) {
          setPoll({
            id: poll.id,
            text: poll.data()!.text,
          });
        } else {
          console.log("couldn't find poll");
        }
      })
      .catch((error) => {
        console.log("error loading poll: " + error);
      });
  }, []);

  return poll;
};

export const usePollAnswers = (pollId: string) => {
  const [answers, setAnswers] = useState<PollAnswer[]>([]);

  //setup data listeners
  useEffect(() => {
    //load all possible answers
    var removeAnswersSnapshot = db
      .collection("polls")
      .doc(pollId)
      .collection("answers")
      .onSnapshot((snapshot) => {
        var answerObjects: PollAnswer[] = [];
        snapshot.docs.forEach((doc) => {
          answerObjects.push({
            id: doc.id,
            text: doc.data().text,
            amount: doc.data().amount,
          });
          setAnswers(answerObjects);
        });
      });
    return () => {
      removeAnswersSnapshot();
    };
  }, []);

  const vote = (id: string) => {
    var newAnswers = [...answers];
    var answer = newAnswers.find((a) => a.id === id);

    db.collection("polls")
      .doc(pollId)
      .collection("answers")
      .doc(answer!.id)
      .set(
        {
          amount: answer!.amount + 1,
        },
        { merge: true }
      );
  };

  return { answers, vote };
};

As you can see we're exporting two functions/hooks which both take a pollId as a parameter.

Let's see how the usePoll hook works:
We declare a local state object of type Poll with the help of the useState hook here. That's basically what we
previously did directly in our page component.
We're doing the actual databasecall in a useEffect hook. The useEffect hook always gets executed when any of the values
in the second parameter changes. As we're passing an empty list as the second parameter, the hook runs when the
component gets mounted.
So we're loading the poll just after the user hits the home page.

db.collection("polls").doc(pollId).get();

This returns a promise containing a firebase documentsnapshot. The contained data of this snapshot is actaully the same
as our Poll type. But we can't just cast it. We need to construct a new Poll object by grabbing each property from the
documentsnapshot:

setPoll({
  id: poll.id,
  text: poll.data()!.text,
});

So now our local state object poll holds the data that we just loaded from the server.
By returning the local poll object from our hook we can get access to this piece of state from outside of the function.
The cool part is, that whenever the local state inside of the hook changes (ie the data got loaded), the returned object
also 'changes' (It doesn't change but it holds the newly set data then).

We abstracted away the actual database loading logic from our page component. We can load polls now from everywhere in
our application by just calling

var myNewPoll = usePoll("myPollId");

Let's get to the usePollAnswers function. It's a little more complicated but if you understood how the usePoll function
works you will not have any problems with the usePollAnswers hook.

We're using local state again. But this time we need to save a list of PollAnswers instead of a single poll. We're
calling the db in a useEffect hook again.
This time we not just get the values we want once, but we're setting up a realtime listener.

var removeAnswersSnapshot = db
  .collection("polls")
  .doc(pollId)
  .collection("answers")
  .onSnapshot(callback);

This will set up a listener on a subcollection of a poll document in our db. A subcollection is basically another
collection that just exists for one single document in our database. Every single poll in the database will have it's
own subcollection of answers that only belong to this particular poll. The callback method will get called everytime a
document in our valueset changes (everytime any answerobject of the poll gets changed).

.onSnapshot(snapshot => {
  var answerObjects: PollAnswer[] = [];
  snapshot.docs.forEach(doc => {
    answerObjects.push({
      id: doc.id,
      text: doc.data().text,
      amount: doc.data().amount,
    });
    setAnswers(answerObjects);
  });
})

In the callback we're looping over every document and construct a new array of PollAnsers. Finally we're saving
the newly constructed array in our local state object.
This time the function we're running inside useEffect returns a function. This is useEffect functionality. Every code in
this function gets called when the component did unmount. We're calling

removeAnswersSnapshot();

there. This is a method that gets returned by the onSnapshot method provided by firestore. Calling this method will
remove the listener from the db. So we're no longer getting updates about database changes as soon as the page
component unmounts.

The last thing we need in order to make our poll app functional is voting functionality.

const vote = (id: string) => {
  var newAnswers = [...answers];
  var answer = newAnswers.find((a) => a.id === id);

  db.collection("polls")
    .doc(pollId)
    .collection("answers")
    .doc(answer!.id)
    .set(
      {
        amount: answer!.amount + 1,
      },
      { merge: true }
    );
};

The vote method takes the id of a PollAnswer as its only parameter. It then looks for the answer in the local state
object by comparing the id. Then it perfoms a write to the firestore database by calling the set method. we just need to
update the amount field of the object. The merge parameter we're passing tells firestore to merge the old object
it currently has stored with the new values we provide. That's all we need to write to the database.

The cool part is, that when we increment the amount field on a poll. Our snapshot listener for the pollAnsers
immediately triggers and updates our UI accordingly. We don't have to do any manual work here.

With this changes done. Open your project path in your terminal and type

npm run start

This will start a development server and you should be able to test the poll app. Open the app in a few browser windows
to see that it syncs in realtime and saves its state.

That's it. We built a fully working realtime poll app with just a few lines of code with the help of react, ionic and
firestore.

If you enjoy this type of content, visit https://gymconsole.app/blog to get more tutorials or connect with me on twitter
@RalliPi.
I'm happy to talk to you or help you with your projects

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.