The Easiest Way To Store Data

The problem with the modern web is that everything moves really fast and complex, feature rich apps became the norm. So, in order to stay competitive, you need to always explore new tools and products that can make your application better with as little effort as possible.

What is RxDB

Being able to quickly test ideas is a must in this competitive environment, and in this video we’ll do just that - we’ll prototype a small security first, peer 2 peer messaging app in under 5 minutes.

You’ll probably love the simplicity of the tech stack. The UI is built with Solid, data is stored locally via RxDB, and the peer 2 peer communication is handled via WebRTC.

Building a web app with Vite, Solid and RxDB

So looking at the architecture of our application, the data will be managed directly by RxDB, and our users will communicate directly on an encrypted channel, with no server in between, thanks to the seamless replication process offered by RxDB. Pretty cool, right?

In a new terminal window let’s initialize a new Vite project using the Solid JS template, and TypeScript as the default programming language. Vite is probably the best front-end build tool you can use these days. It really speeds up the development process, and comes packed with a wide range of features which led to the adoption of Vite in quite popular JS projects.

$ npm create vite@latest awesome-rxdb -- --template solid-ts

On the UI, things are straight forward. We’ll define a Solid function component and a couple of signals to manage the user’s handle, messages and joined channel.

function App() {
  const [view, setView] = createSignal<View>(View.Setup);

  const [channel, setChannel] = createSignal("one");
  const [handle, setHandle] = createSignal("");

  const [messages, setMessages] = createSignal<string[]>([]);
  const [message, setMessage] = createSignal("");
}

Signals are powering Solid’s reactivity. Hopefully, you are familiar with them by now, since they got integrated in most popular frontend frameworks in recent years.

When the component is mounted in the DOM, we’ll fetch the messages from the database.

onMount(async () => {
  // ...
});

Then, we’ll add some basic event listeners and the UI elements using JSX.

async function onSend(ev: KeyboardEvent) {
  if (ev.key === "Enter") {
    saveMessage(handle(), message());
    setMessage("");
  }
}

async function startSession() {
  setView(View.Chat);
  startReplication(channel());
}

Note that since this is Solid, we are using the special Show and For components which ensures that DOM elements will be updated in an efficient manner when changes are detected.

<Show when="{view()" ="" ="" ="View.Chat}">
  <h3>#{channel()}</h3>
  <ul>
    <For each="{messages()}"
      >{msg =>
      <li><span>{msg}</span></li>
      }
    </For>
  </ul>
  <footer>
    <input placeholder="Message..." value="{message()}" onChange="{ev" ="" />
    setMessage(ev.target.value)} onKeyUp={onSend} />
  </footer>
</Show>

Now, for the fun part, let’s look at RxDB, the sponsor of this video in more detail, and see how exactly it can help us exchange messages via multiple peers, in real time, all in just a few lines of code.

In a new typescript file I am defining the structure of our messages table.

export const messagesSchema = {
  primaryKey: "id",
  type: "object",
  properties: {
    id: {
      type: "string",
      maxLength: 36,
    },
    name: {
      type: "string",
    },
    message: {
      type: "string",
    },
    timestamp: {
      type: "string",
      format: "date-time",
    },
  },
  required: ["id", "name", "message", "timestamp"],
};

We’ll need a unique id, the body and the author of the message. The timestamp will help us sort incoming messages in the appropriate order.

Then, in the database.ts file let’s create an Rx database.

// database.ts
addRxPlugin(RxDBDevModePlugin);

export const db = await createRxDatabase({
  name: "awesome",
  storage: getRxStorageDexie(),
});

Note that RxDB is not a self contained database. Instead the data is stored in an implementation of the RxStorage interface. This allows you to switch out the underlying data layer, depending on the JavaScript environment and performance requirements. So you’ll be able to use the SQLite storage for a Capacitor App or IndexedDB in browser based applications.

If you are not familiar with Indexed DB, you are missing out!

Indexed DB

This is a low-level API for client side storage which can handle significant amounts of structured data, including files and blobs. While the Local Storage is useful when handling small amounts of strings, Indexed DB uses indexes to enable high-performance querying on large amounts of data. It provides support for the usual create, read, update and delete operations, enabling applications to work well both online, and offline.

const db = e.target.result;
const store = db.createObjectStore("store", { keyPath: "id" });
store.createIndex("nameIndex", ["name"], { unique: true });

const store = transaction.objectStore("store");
const index = store.index("nameEmailIndex");

store.add({ id: 1, name: "Awesome", email: "hi@awesome.club" });
index.get(["Awesome"]);
store.delete(1);

Back to our database file, let’s register the messages collection.

// database.ts
await db.addCollections({
  messages: {
    schema: messagesSchema,
  },
});

Then define some service methods for our messages table.

// database.ts
export async function saveMessage(name: string, message: string) {
  await db.messages.insert({
    id: randomCouchString(10),
    name,
    message,
    timestamp: new Date().toISOString(),
  });
}

export async function removeMessage(id: string) {
  const doc = await db.messages.findOne({ selector: { id } }).exec();
  if (doc) {
    await doc.remove();
  }
}

More importantly however, I want to define and start a replication process, which will take care of the peer 2 peer communication for me.

Data replication through WebRTC

We can do this easily by employing the WebRTC plugin. I’m simply defining the collection we want replicated and a few other configuration details.

export async function startReplication(channel: string) {
  return await replicateWebRTC({
    collection: -db.messages,
    topic: channel,
    connectionHandlerCreator: getConnectionHandlerSimplePeer({
      signalingServerUrl: "wss://signaling.rxdb.info/",
      webSocketConstructor: WebSocket,
    }),
  }).then((replicationState) => {
    replicationState.error$.subscribe((err: any) => {
      console.error(err);
    });
    replicationState.peerStates$.subscribe((s) => {
      console.log("=> new peer");
    });
  });
}

RxDB is pretty flexible when it comes to replication as well. I am relying on WebRTC in this example, but there are many other options you can employ here to address your specific application needs.

Ok. Now, we can go back to our UI component, and put everything together. Of course, I want to fetch any existing messages when the app is mounted in the DOM.

onMount(async () => {
  db.messages
    .find({
      sort: [{ timestamp: "asc" }],
    })
    .$.subscribe((data) => {
      setMessages(data.map((it) => it.get("message")));
    });
});

Note that RxDB is built on top of the Reactive Extensions library, so all query results offer observables you can subscribe to. In our case when new messages are entered in the collection via replication we’ll react accordingly, update the messages signals value, which, in turn, will update the UI.

Thanks again to Daniel for his contributions on RxDB and for sponsoring this video. As we all know, open source is hard work, so be sure to go ahead and check out his repo if you have a few minutes to spare.

If you feel like you learned something, you should watch some of my youtube videos or subscribe to the newsletter.

Until next time, thank you for reading!