Real-time data with xState, Firebase and Next.js
Tags: webdev, xstate, firebase, nextjs
Published: 2023-10-16 13:48:57

### TL;DR
If you just want to see the code, it is [here](https://github.com/g-todorov/xstate-real-time-data-example).

### Background

After successfully implementing the [authentication flow with Firebase](https://dev.to/gtodorov/authentication-with-xstate-firebase-and-nextjs-4h2), my next focus for the web app is real-time updates, which I intend to achieve using Cloud Firestore.

### Use case 

Building upon our existing codebase, we will add functionalities for logged-in users. This includes creating a mechanism for writing and reading messages from the database, with real-time updates.

### Disclaimer

For simplicity, the message text will be hardcoded. Form input and validation will be covered in a future blog post. After fetching the messages list, we will only display the number of messages.

### Setting data in Firestore

The messages will consist of two properties: `text` and `sender`. The more interesting property is `sender`, which will hold the `uid` of the authenticated Firebase user who created the message.

```typescript
interface Message {
  sender: string;
  text: string;
}
```

In the `userSubscriber` service, we will extract the `uid` from the `user` parameter and pass it to the `GO_TO_AUTHENTICATED` event. After adding this transition, we can introduce a "setUid" action that reads it from the `event` object and sets it in the machine's context.

```typescript
const appMachine = createMachine(
  {
    context: { uid: "" },
    /* ... */
    invoke: { src: "userSubscriber" },
    on: {
      GO_TO_AUTHENTICATED: {
        actions: ["setUid"],
        target: "authenticated",
        internal: true,
      },
    },
    /* ... */
    initial: "loading",
    states: {
      loading: { tags: "loading" },
      authenticated: {
        /* ... */
      },
      /* ... */
    },
  },
  {
    actions: {
      setUid: assign({
        uid: (context, event) => {
          return event.uid;
        },
      }),
      /* ... */
    },
    services: {
      userSubscriber() {
        return (sendBack) => {
          const unsubscribe = onAuthStateChanged(auth, (user) => {
            if (user) {
              sendBack({ type: "GO_TO_AUTHENTICATED", uid: user.uid });
            } else {
              sendBack({ type: "GO_TO_UNAUTHENTICATED" });
            }
          });
          return () => unsubscribe();
        };
      },
      /* ... */
    },
  }
);
```

Now, we can proceed with creating the actual message. First, we need UI elements that trigger the event. In the root page, we add a button that initiates the process:

```JSX
<button
  onClick={() => {
    send({ type: "ADD_MESSAGE" });
  }}
>
  "Add message"
</button>
```

We need to implement the `ADD_MESSAGE` event within the `authenticated` state to ensure it's available only to existing users. This event targets the `.addingMessage` substate, which, in turn, invokes the `addMessage` service.

We use the `setDoc` Firestore method and we want our newly created messages to be organized in a messages collection. Each message will be a document with its id based on the current time in milliseconds. However, it's important to note that this approach is not recommended for production as there is still a technical chance of duplicating IDs. The message itself consists of a `text` value (currently hardcoded) and a `sender` that reads the authenticated user's ID from the context.

Don't forget to set your database access permissions.

```typescript
import firebaseApp from "@/firebase";

/* ... */
const firestore = getFirestore(firebaseApp);

const appMachine = createMachine(
  {
    /* ... */
    initial: "loading",
    states: {
      loading: { tags: "loading" },
      authenticated: {
        invoke: { src: "messagesSubscriber" },
        on: {
          ADD_MESSAGE: { target: ".addingMessage" },
        },
        initial: "idle",
        states: {
          idle: {},
          signingOut: {
            invoke: { src: "signOut", onDone: { target: "idle" } },
          },
          addingMessage: {
            invoke: {
              src: "addMessage",
              onDone: { target: "idle" },
              onError: {
                actions: ["logError"],
              },
            },
          },
        },
      },
      /* ... */
    },
  },
  {
    /* ... */
    services: {
      /* ... */
      async addMessage(context, event) {
        await setDoc(doc(firestore, "messages", `${Date.now()}`), {
          sender: context.uid,
          text: "Lorem Ipsumm",
        });
      },
    },
  }
);
```

### Reading data from Firestore

With messages now being written to the database, we can proceed to demonstrate the power of Firestore in real-time updates.

To do this, we use the `onSnapshot` method, which attaches listeners to the parts of the database we need to monitor for changes and returns the newly updated data.

Similar to the `onAuthStateChanged` listener, we utilize the `onSnapshot method` in a service callback. From there, we can set the retrieved messages in the context by triggering the `SET_MESSAGES`. This allows us to easily subscribe to the messages collection and provide the latest information to all authenticated users.

```Typescript
const appMachine = createMachine(
  {
    context: { messages: [] },
    /* ... */
    initial: "loading",
    states: {
      loading: { tags: "loading" },
      authenticated: {
        invoke: { src: "messagesSubscriber" },
        on: {
          /* ... */
          SET_MESSAGES: { actions: ["setMessages"] },
        },
        initial: "idle",
        states: {
          idle: {},
          /* ... */
        },
      },
      /* ... */
    },
  },
  {
    actions: {
      /* ... */
      setMessages: assign({
        messages: (context, event) => {
          return event.messages;
        },
      }),
    },
    services: {
      /* ... */
      messagesSubscriber() {
        return (sendBack) => {
          const unsubscribe = onSnapshot(
            query(collection(firestore, "messages")),
            (querySnapshot) => {
              const messages: Message[] = [];
              querySnapshot.forEach((doc) => {
                messages.push(doc.data().text);
              });

              sendBack({ type: "SET_MESSAGES", messages });
            }
          );

          return () => unsubscribe();
        };
      },
      /* ... */
    },
  }
);
```  

From this point, you can display your messages in a way that suits your application. In our case, we choose to display only the number of messages in the collection next to the "Add message" button.

```JSX
const [state, send] = AppContext.useActor();

<span>{state.context.messages.length}</span>
```

### Conclusion

Overall, this example sets the foundation for building real-time applications that react to dynamic data changes. From what I've experienced, the possibilities for expanding and refining this setup are extensive, making it a solid choice for developing interactive web applications.