Paul Jones

full-stack application programmer

Experimental Frontend Application Development

Far from being simple, simple is hard.

A calculator is the kind of simple everyone understands, and so is the idea that food is nutritious, and maybe even making calculations of the nutrients in food. JavaScript, as as far programming languages go, is also simple. If things get complex, the JavaScript ecosystem offers an abundance of choices. Further, web browsers are extraordinarily complex software tools, enabling the creation of applications ranging from spreadsheets to 3D games—tasks that once demanded specialized, standalone software. As a full-stack developer, most of my projects have been networked CRUD applications, which interact RESTfully with backend services and display information to users through a UI framework. While I find this development paradigm enjoyable and efficient, I’ve been eager to push the boundaries of my web development knowledge. Thus, I embarked on a personal project using unconventional tools, leading to the creation of Nutrition Planner—a venture into less explored (by me) areas of web development, and this is a record of one of those trips.

Painting pixels

At its core, Nutrition Planner is a calculator, perhaps even a simple one. The app is built around just two primary data structures: an item and a sub-item. An item is characterized by an ID, date, nutritional information (such as calories, serving size, and macronutrients), and its cost in cents. A sub-item, on the other hand, consists of an ID, a quantity, and the ID of its associated item. Leveraging these fundamental structures, I aimed to develop an application that encompasses a log, an item library, a recipe creator, and a planner.

However, my goals extended beyond merely creating an application capable of operating within a single session, displaying information to the user, and then erasing its memory upon reload. I want an app that can synchronize across multiple devices, accessible whether on the web, mobile, or desktop platforms, With these dual objectives of simplicity and comprehensive platform integration in mind, I embarked on the search for the appropriate tools.

Starting with my preference for utilizing robust front-end frameworks such as Bootstrap, Foundation, MaterialUI, Ant Design, etc., all of these are great tools. Of particular note is their size, which by modern standards isn’t at all large, these frameworks are highly optimized and offer remarkable capabilities. However, their complexity is undeniable, encompassing extensive classes, lines of code, API, and parameter counts. In most professional settings, leveraging and customizing these tools often demands a full-time specialist’s attention.

Contrastingly, Chakra UI presents a refreshing deviation from this norm. It might appear almost simplistic or “toy-like” at first glance, but this observation isn’t meant as criticism. Instead, I find its simplicity refreshing. Chakra UI strikes a balance, offering just enough opinion to guide design without overwhelming the user with superfluous features. It provides straightforward components for common needs like GridModalTableButton, and Form. Compared to other frameworks I’ve used, working with Chakra UI feels akin to using “stock” React—it’s intuitive, efficient, and unobtrusive, maintaining a “normal” look and feel. I stumbled across it while learning about web development one day, and decided to use it for this project.

This decision led me to commit to using React for this project. Although Chakra UI doesn’t exclusively require React, and can be adapted to other frameworks or libraries, my decision was influenced by React’s widespread adoption and my positive past experiences with it. This combination of React’s versatility and Chakra UI’s simplicity seemed perfectly suited for the development of my application, aligning with my goal of creating a user-friendly and accessible project.

Multi-platform

After deciding on Chakra UI as the UI framework for the project, my next step was to identify a suitable project structure or boilerplate that could streamline the development process, addressing aspects such as page layout, routing, building, and deploying the application. Starting with the latter concern, it was around this time that I became interested in Electron, a project created by GitHub. Electron facilitates the use of the Chromium engine as an “application runtime,” enabling developers to distribute their web applications as standalone, “headless browser”-like desktop applications. Essentially, it allows JavaScript applications to run independently outside of the traditional web browser environment.

I was attracted to Electron as a solution for encapsulating a simple website within a package that mimics the appearance and behavior of a native desktop application, especially when paired with a native font stack to give that “home-y” feel. The opinions on using Electron for such purposes are varied, with some critics suggesting that a website should suffice for most cases. While I understand and even agree with these arguments, my choice to explore Electron was driven by a desire for experimentation and the unique learning experience it offers, rather than out of any practical necessity. By adopting Electron, I aimed to create an application that not only serves its intended purpose but also provides a distinct user experience, akin to that of a native application, purely for the enjoyment and challenge it presents.

Addressing the desktop experience through Electron was just one part of my plan. Equally important was my goal to support mobile devices in a manner that feels native. To bridge this gap, I aimed to ensure that the web application, while operational within Electron for desktop environments, would also function as a “progressive web app” (PWA) for mobile users. PWAs have been written about extensively elsewhere, so I won’t re-iterate how great they can be here, the links provided should be sufficient.

Meta-frameworks

Transitioning to the development phase where both desktop and mobile platforms are supported, my next challenge involved crafting the website that would double as the application in its “native” form. Given my choice of Chakra UI with its React integration, it became imperative to find a solution for managing the application’s routing and behavior within the React ecosystem. Previously, I had relied on React-specific tools like React Router to simulate a content management system using JSX, a process I often found cumbersome.

During my exploration of potential solutions, I discovered Next.js. Hardly a small-time niche tool, its comprehensive documentation and adoption by numerous reputable companies caught my attention, prompting me to experiment with it in this project. Next.js appealed to me as a learning opportunity, even if it was somewhat of a departure from the project’s primary requirements. In retrospect, while Next.js introduced me to concepts like server-side rendering and authentication—features not directly relevant to my project’s goals—it provided a straightforward mechanism for defining routes and behaviors. For example, creating a file named items.tsx effortlessly generated a corresponding URL /items and facilitated easy linking within the app.

Having gained more experience with Next.js since then, I’ve come to view it as somewhat of an overkill for simpler projects, given its expansive features set that includes multiple routing and rendering methods. This complexity can sometimes contribute to a bloated developer experience, straying from the simplicity I initially sought. Nevertheless, Next.js proved to be a valuable tool, enabling me to define and implement the application’s structure and navigation effectively, even if it wasn’t the perfect fit in hindsight. Its utility in this context underscores the importance of selecting the right tools based on the specific needs and goals of a project, a lesson that has informed my approach to web development moving forward. Additionally, while I’ve read that Vercel’s (the company behind Next.js) deployment solution gets expensive at large scales, but is perfect for the hobbyist with an open-source project.

Reactive database

Having established the means to deploy the application across web, mobile, and desktop platforms, and to create UI elements that are consistent across these environments, my next challenge was to manage data in a way that supported both offline functionality and optional data synchronization with an external endpoint. My goal was to create an offline-first application that allowed users the flexibility to integrate their own backend solutions if they chose to do so. This led me to discover RxDB, a solution that perfectly aligned with my project requirements.

RxDB is a reactive, offline-first database library designed for real-time applications. It supports a variety of storage backends and offers seamless replication capabilities. Initially, RxDB could use PouchDB for local data storage, leveraging the browser’s IndexedDB for data persistence. This setup facilitated straightforward replication with a remote CouchDB server, providing a sync mechanism that was both efficient and easy to implement. The architecture of RxDB, with its emphasis on reactivity and offline accessibility, made it an ideal choice for my project.

However, the learning curve was steep, especially with the intricacies of configuring PouchDB and CouchDB, as well as understanding the underlying IndexedDB storage mechanism. The introduction of major version 14 of RxDB brought significant changes, including a dedicated CouchDB replication plugin and improved support for IndexedDB, either directly or via Dexie.js. These updates aimed to simplify the database management experience and expand the library’s capabilities. For the first version of the app, which used RxDB 12, I used its PouchDB plugin configured to store locally in IndexedDB, replicating to CouchDB if there was an available URL configured.

Getting started

With the architectural foundation for the application firmly in place, the next step was the actual development work. This phase was significantly expedited thanks to discovering Nextron, a project that seamlessly integrates Next.js, Electron, and Chakra UI (or other UI frameworks). Nextron provided a pre-configured template that bridged these technologies, offering a straightforward starting point for the project. This integration facilitated the creation of an application that could run on desktop environments via Electron, while also leveraging the design and development efficiencies of Next.js and Chakra UI.

Upon incorporating RxDB along with the necessary RxDB Hooks for React, I began developing the data layer and UI components of the application. The UI components were deliberately kept simple to maintain the project’s focus on ease of use and straightforward functionality. One notable exception was the implementation of an “infinite scroll” mechanism for rendering a table, inspired by the “table view” in iOS or “list view” on Android. Although a data table with navigation buttons might have been a more conventional, and manageable, choice for this purpose, I opted for the infinite scrolling approach to preserve the application’s simplicity for the user.

Additionally, I integrated “react-big-calendar” (interestingly from the same programmer who maintains Yup, and a fellow New Jerseyan) to quickly set up the log view, aiming for a user-friendly and visually appealing interface for managing and viewing entries. This choice, while perhaps unconventional, proved effective, enabling the rapid deployment of a functional log view with minimal debugging required.

The development process, guided by the principles of simplicity and functionality, highlighted the value of selecting the right tools and libraries to meet the project’s goals. By combining the strengths of Nextron, RxDB, Chakra UI, and other React components, I was able to create an application that not only met the initial requirements but also offered a seamless and intuitive user experience across desktop and mobile platforms.

The first sign of trouble

The initial foray into developing the data layer of the application revealed an unexpected and challenging aspect that complicated the integration with TypeScript: the recursive nature of the data structure. Its recursive because Items have Sub-items which have Items-and so on. This recursive design was essential for achieving the desired functionality, where “recipes” or “groups” could contain not only items but also other groups. Similarly, “plans” could be comprised of items, groups, or a combination thereof, with “logs” including plans, groups, or items. Despite all these entities ultimately being treated as Item objects, their recursive relationships posed a significant challenge for type validation within TypeScript.

This complexity was compounded by the use of Yup schemas for form validation and the corresponding definition of RxDB schemas for database structuring and versioning. The recursive data model led to an issue where TypeScript’s type validation became infinitely recursive, making it impossible to complete at “compile” time. This problem had no direct impact on the correctness of the application’s functionality but presented a significant obstacle for type-checking during development.

As a result, I was forced to disable TypeScript’s type-checking to proceed with development. This workaround, however, came with drawbacks, notably undermining many of the advantages TypeScript offers, such as enhanced code reliability and developer productivity through static type-checking. Additionally, the issue adversely affected the development environment’s tooling, causing operations like code formatting and the application of “quick fixes” to experience intolerable delays.

Navigating this challenge highlighted the complexities of working with recursive data types. Despite these hurdles, the project moved forward, albeit with concessions made in terms of the development experience and the benefits typically afforded by TypeScript’s type safety features.

I also discovered another more minor problem with my data model, and that’s that while it was desirable to change the “downstream” prices and nutrition in the case of modifying recipes and plans, it was not desirable for logs. The need to preserve the integrity of past entries, under the obvious premise that “you can’t change the past,” led to the implementation of a deep-copy system for logging purposes. This system ensured that any modifications made to items or groups would be accurately reflected in current and future plans without retroactively affecting the historical records in the log.

Despite the difficulties posed by the absence of type-checking, I persevered, successfully bringing the application to a functional state. Users could add items to the database by importing nutrition information, create groups from these items (and potentially other groups), form plans from these groups and items, and generate logs from all the aforementioned entities. These logs were then elegantly displayed on a calendar, offering users a comprehensive view of their activities and plans.

The architectural decisions made throughout the development process resulted in a versatile application that could be experienced as a native desktop application, a progressive web app on mobile devices, and a fully-featured website. The project, thus, achieved its goal of creating a unified, cross-platform solution that leverages modern web technologies to provide a seamless, user-friendly experience.

A syncing feeling

The second significant challenge I encountered was related to data synchronization, specifically with integrating CouchDB as the replication backend for the Nutrition Planner. Having no prior experience with CouchDB, which was still relatively new at the time of implementation, presented a steep learning curve. Initially, CouchDB offered a more lenient security model, including default settings where all users had “admin” rights—a convenience feature that had recently been revised, complicating user management.

I managed to deploy CouchDB on Linode and configured it for secure network access to enable replication. However, one notable limitation was the absence of an automated process for provisioning new users directly via a URL; instead, it required manual intervention through Fauxton to create each new user. This aspect was somewhat disappointing, given my goal for a more seamless user experience.

Despite this hurdle, the flexibility of CouchDB offered a compelling advantage: the application could connect to any functional CouchDB endpoint for data replication. This architecture allowed for a “personal cloud” experience, where users had the option to utilize a managed service backend provided by the application or to “bring their own” backend. This approach empowered users with full control over their data, enhancing the application’s privacy and customization options. Users could not only ensure the security of their data but also leverage it across other applications if desired.

This dual capability—offering a managed service for ease of use or allowing users to host their own CouchDB instances—highlighted the application’s versatility and its potential to serve a wide range of user preferences and needs. It underscored the project’s commitment to user data autonomy and privacy, providing a foundation for a more personalized and secure user experience.

A year or so later

Everything described before this was the initial development work, which at time of writing was over a year ago. Bringing the narrative up-to-date, as of a few days ago, I decided to update Nutrition Planner with the latest reasonable dependencies, for the same of maintaining it in something of a presentable state. The transition away from using PouchDB as an intermediary for CouchDB synchronization, as dictated by the major changes in RxDB version 14, marked a significant pivot in the application’s development. RxDB’s decision to drop PouchDB support was based on its implementation’s performance issues, a move I found entirely reasonable given the aim for efficiency and reliability in data synchronization.

The relative lack of widespread adoption for CouchDB, compared to other database services, and the complexities involved in its deployment and configuration, further validated the need for a shift in the application’s backend strategy. CouchDB’s niche appeal, primarily among enterprises and enthusiasts, underscored the importance of exploring more accessible and user-friendly options for data replication.

Fortunately, RxDB’s support for Firebase Firestore as an alternative replication target presented a viable path forward. Coupled with the option to use Dexie for interacting with IndexedDB, this transition seemed straightforward—at least in theory. The process involved updating the application’s dependencies and replacing the PouchDB and CouchDB integration with Dexie and Firestore, respectively. With Firebase, users could create their own instance and add the relevant parameters in the settings tab of the application. While this is a (relatively) good user experience, because it’s very easy to spin up a Firestore, it comes with the obvious and inevitable risk that one day Google will kill or charge for Firebase.

This is not a big deal relative to my next finding, unfortunately. While the application successfully writes data to Firestore, it seems to be unable to pull from Firestore. This problem persisted despite the configuration appearing correct, with the occasional exception of using an in-memory store for document management. Such a workaround, though effective for ensuring data receipt from Firestore, was impractical for the application’s intended offline-first and data-intensive use case. There will be some line of code responsible for this error, probably even one I wrote, but I think the fundamental cause is more intangible, more to do with the fact that there’s IndexedDB, with its own quirks, and Dexie.js, with its own quirks, and Firebase Firestore, with its own quirks. Ideally, there would be some software solution that used IndexedDB to persist locally, and when available, sync that IndexedDB to somewhere remote if the user sets one. While not at all an option in this environment, Apple’s “Core Data with CloudKit” strikes me as an enviable API.

Throw away one

Reflecting on the journey and the choices made, there are several areas where I would consider alternative approaches if I were to embark on this project anew, even with its status as a technological sandbox. One such reconsideration would be the infinite scrolling table used to display items, groups, and plans. While functional, its performance degrades with larger data sets, primarily due to the lack of virtualization, risking memory overflow and degraded user experience.

Additionally, while Nextron provided an invaluable starting point with its seamless integration of Next.js and Electron, its lag behind the latest versions of Next.js introduces potential limitations. This factor, combined with my reservations about the full suitability of Next.js for this project, prompts me to explore other boilerplates, such as the Electron React Boilerplate, known for its robust support and up-to-date practices.

Despite these considerations, my familiarity with Next.js and its routing capabilities, honed through extensive use, suggests its retention in future iterations of the project. Separately, my experience with Material UI has grown, recognizing its value as a comprehensive UI framework despite its relative heft. The framework’s data table component, in particular, stands out for its functionality and could address some of the current application’s UI limitations. For a more fun and mobile-minded application, I’d be interested to try Framework7.

Navigating through the complexities of modern web development, as highlighted in the journey of creating the Nutrition Planner, reveals that paradox I opened with. The process of rendering UI elements and managing data storage presents its own set of challenges, yet these tasks pale in comparison to the intricacies of user management and data replication across devices and platforms.

While drawing pixels on a screen is a well-understood problem with numerous effective solutions, and similarly, storing information in IndexedDB, perhaps less common than other storage methods, has still myriad good tools. However, my quest for an open-source, easily deployable solution for user provisioning and seamless replication of IndexedDB data remains a significant challenge. While RxDB and Dexie offer paid features for data synchronization in their premium offerings, and CouchDB provides a framework close to the ideal, there remains a gap in the ecosystem (or at very least my knowledge of it) for a solution that combines ease of deployment with the flexibility and security necessary for user data management. (And while Firebase’s Firestore is basically what I want, its disqualified for idealogical reasons.)

The exploration of these technologies, despite the occasional sense of being overwhelmed by the vast array of tools and frameworks, enriched my understanding of what is possible. And having spilled all this ink documenting my research into creating a simple application to calculate the cost and nutrition of my lunch, I return to that too familiar paradox I opened with:

Far from being simple, simple is hard.

Have an idea?

Let’s create something new