NEW: Heap for mobile. Track every interaction, on every platform.

Learn more
skip to content
Loading...
    • The Digital Insights Platform Transform your digital experience
    • How Heap Works A video guide
    • How Heap Compares Heap vs. competitors
    • The Future of Insights A comic book guide
  • Data Insights

    • Session Replay Complete context with a single click
    • Illuminate Data science that pinpoints unknown friction
    • Journeys Visual maps of all user flows

    Data Analysis

    • Segments User cohorts for actionable insights
    • Dashboards Share insights on critical metrics
    • Charts Analyze everything about your users
    • Playbooks Plug-and-play templates and analyses

    Data Foundation

    • Capture Automatic event tracking and apis
    • Mobile Track and analyze your users across devices
    • Enrichment Add context to your data
    • Integrations Connect bi-directionally to other tools

    Data Management

    • Governance Keep data clean and trusted
    • Security & Privacy Security and compliance made simple
    • Infrastructure How we build for scale
    • Heap Connect Send Heap data directly to your warehouse
  • Solutions

    • Funnel Optimization Improve conversion in user flows
    • Product Adoption Maximize adoption across your site
    • User Behavior Understand what your users do
    • Product Led Growth Manage PLG with data

    Industries

    • SaaS Easily improve acquisition, retention, and expansion
    • eCommerce Increase purchases and order value
    • Financial Services Raise share of wallet and LTV

    Heap For Teams

    • Product Teams Optimize product activation, conversion and retention
    • Marketing Teams Optimize acquisition performance and costs
    • Data Teams Optimize behavioral data without code
  • Pricing
  • Support

    • Heap University Video Tutorials
    • Help Center How to use Heap
    • Heap Plays Tactical how-to guides
    • Heap Updates
    • Professional Services

    Resources

    • Blog A community for digital builders
    • Content Library Ebooks, whitepapers, videos, guides
    • Press News from and about Heap
    • Webinars & Events Virtual and live events
    • Careers Join us

    Ecosystem

    • Customer Community Join the conversation
    • Partners Technology and Solutions Partners
    • Developers
    • Customers Over 8,000 successful companies
  • Free TrialRequest Demo
  • Log In
  • Free Trial
  • Request Demo
  • Log In

All Blogs

Engineering

How We Write Front-end Code

Anojh Gnanachandran
February 20, 20187 min read
  • Facebook
  • Twitter
  • LinkedIn

Writing front-end code in a sufficiently complex web app has never been an easy task. With all the view, state management, and routing libraries out there, it can be hard to know how best to fit the pieces together. Through writing many thousands of lines of code for the Heap web app, we’ve found that certain principles have allowed us to move fast and ship powerful features without compromising on testability or extensibility. We’d like to share what we’ve learned with the hope that applying these principles helps you do the same.

Interested in learning more about Heap Engineering? Meet our team to get a feel for what it’s like to work at Heap!

Front-end Tech Stack

When Heap was started in 2013, CoffeeScript and Backbone were popular options that allowed us to become productive quickly. Over time, we realized we preferred the type safety guarantees provided by TypeScript, and the iteration speed offered by React. While we still have some Backbone in our codebase, most new features are written in TypeScript and React.

Like many web apps, Heap’s domain models (like events, reports, and dashboards) need to be stored, retrieved, and passed around to different components. We handle this state management with a library called MobX. We use multiple domain stores, each of which is responsible for storing their own domain model data. In Heap, this means there’s a store for holding event definitions, a separate store for holding reports, and so on.

Architecture

Heap's Frontend Architecture

When you visit a page on the Heap web app, the request goes through our Backbone routing layer. From there, the routing layer chooses a React container component to render based on the URL. Container components retrieve the data they need from MobX stores and send it down via React props to presentational components. When a user interacts with the UI, the callback for the event handler eventually reaches a MobX store which may interact with the transport layer (e.g. to persist an update to our database), and/or update its local domain model state.

To make this flow more concrete, we’ll go through an example of how we might write a small feature in the Heap web app from scratch. Along the way we’ll talk in more depth about the principles underlying our front-end architecture, and how they make our code easy to test and extend.

Building the Frontend for a Notification System

Suppose we’re writing the frontend for a notification system within Heap. If a user executes a complex, long-running query, we might want to let them run additional queries while waiting for their first query to come back. We can let users know when their queries complete by showing them a notification. For the purposes of this example, the user should be able to view their notifications and mark them as read.

Before we proceed, there are some MobX-specific annotations we use that are important to understanding the code below. Functions annotated with @computed are derived from properties marked as @observable. These functions are assumed to be pure, and so MobX can cache their results. If any properties marked with @observable change, any @computed functions that rely on their value will be re-computed. The fact that this happens automatically through MobX turns out to be super useful since it means we get caching for free, and it reduces the amount of actually modifiable state.

We also make extensive use of Lodash (denoted by an underscore in the code examples below). Lodash provides several utility functions which help make our code more declarative and succinct. We’ll use some of these functions when we write our MobX store.

Models and Stores

We’ll start with thinking about what our notification data model might look like. For simplicity, we’ll say that a notification has an id, some textual content, a creation time, and some state indicating whether it was read.

We’ll store a user’s notifications as a map from ID to notification.

We’ll initialize the NotificationsStore in a special store called the root store. This root store is a singleton which holds references to instances of the various domain stores in our app.

Root Store > Singleton Stores

In the past, we wrote all of our stores as singletons. While singletons have their advantages, they are notorious for making testing difficult. It’s easier to construct stores with the data we want in our unit tests than trying to mock out the global state contained in singleton stores.

Another problem with using singleton stores was that the dependency graph between stores was often unclear. Stores were created in some non-obvious order and there was no way to declare which stores depended on other stores. Since domain models often reference other models, so do their corresponding stores.

Dependencies are made much more obvious by the order of initialization in a root store. The rule we follow is that any store is allowed to depend on any other store created before it. We can make these dependencies even more explicit by directly passing in the store instance when instantiating other stores. For example, if the NotificationsStore wanted to make calls to functions defined in a QueriesStore, we could do something like NotificationsStore.initialize(notifications, this.queriesStore) as long as this.queriesStore had already been initialized. This sort of dependency injection is used throughout our codebase and makes testing a breeze, as we’ll see later on.

Now that we have our data in MobX stores, we’re ready to start displaying notifications to the user.

Container and Presentational React Components

When designing React components, we’ve found the separation of container and presentational components to be invaluable. These components are described in detail in Dan Abramov’s article, but we’ll summarize the differences below.

Container components are responsible for retrieving data from MobX stores and passing the data down as props to their child presentational component. Presentational components are then responsible for actually rendering HTML to the DOM. This separation of concerns makes our code much easier to understand. It also allows for reusability since presentational components can be used with different data sources.

Now that we’ve talked about the two different kinds of components, we’ll look at NotificationsViewContainer. This container simply passes down data from the NotificationsStore to the presentational NotificationsView.

You’ll notice we marked this container as being an @observer. This annotation comes from a MobX extension for React. A component marked with @observer is automatically re-rendered whenever any of the @observable properties (including @computed functions) it uses in its render() function are updated. So when the state of read and unread notifications is updated, NotificationsViewContainer is automatically re-rendered with the new data. We can annotate container components with @observer to ensure the data they present is always up to date without having to write any additional code.

You’ll also notice that instead of directly calling methods on an imported singleton store, we pass the store instance itself as a prop to this top-level container component. This dependency injection makes it explicit what data is required by each of our components. This also simplifies unit testing of components. We can construct exactly the stores the components depend on without worrying about any hidden dependencies.

Now that we have our container component set up, we’ll look at the presentational NotificationsView that actually renders the read and unread notifications.

Clicking on an unread notification calls the onMarkAsRead function provided as a prop to this component, which in turn calls the appropriate store function to mark the notification as read. Finally, NotificationsViewContainer re-renders in response to the readNotifications() and unreadNotifications() being updated and the NotificationsView immediately reflects the new state!

Testability

Now that we’ve written the code for our models, stores, and components, it’s time to add a couple unit tests. We’ll use Mocha, Chai, and Enzyme to test our container component:

Here, we construct a NotificationsStore with the exactly the data we want and pass it to NotificationsViewContainer as a prop. We then render our component using Enzyme’s mount() function. Finally, we assert that a NotificationsView is rendered with the props we expect.

The alternative to passing NotificationsStore as a prop would be to directly import the RootStore in NotificationsViewContainer. Testing this component with mock data would then require mocking out the import itself, which while possible, is far from ideal. It’s much more obvious what’s going on when we construct the store instance and pass it to the component under test.

Testing the NotificationsStore is similarly straightforward. We’ll initialize a store with some mock data, and assert that the read and unread notifications are as we expect.

Extensibility

This architecture makes our stores and components easier to extend in a couple of ways:

  1. We can pretty easily create new domain stores by following the same pattern as our NotificationsStore. We’d just have to pass in any dependencies our new store relies on, and initialize it in the root store. If we want a container component to use this store, it’s as easy as passing in that store instance as a prop.

  2. The layer of abstraction between stores and presentational views means changes to our information schema (e.g. using different data structures to hold our domain models) can be made reliably without updating our presentational components. Similarly, we can modify presentational views without having to update any of our stores or container components.

Final Remarks

MobX and principles like dependency injection have helped us tremendously toward the goal of having a codebase we can be proud of. These principles allow us to work on new and existing features while ensuring testability and extensibility, and we hope they help you too. We’d love to hear what’s worked (and what hasn’t worked) for you, so feel free to reach out to us on Twitter. Finally, if this sort of thing interests you, we’d love to have you join us at Heap!

Anojh Gnanachandran

Was this helpful?
PreviousNext

Related Stories

See All

  • Heap.io

    Data Stories

    Celebrating H&R Block as the inaugural winner of the Digital Innovator Award

    March 22, 2023

  • Heap.io

    Product Updates

    Introducing Heap for mobile: see Everything, Everywhere all at once

    March 14, 2023

  • Heap.io

    Data Stories

    How I shipped a mobile app without tracking and bad things™ happened

    March 15, 2023

Subscribe

Sign up to stay on top of the latest posts.

Better insights. Faster.

Request Demo
  • Platform
  • Capture
  • Enrichment
  • Integrations
  • Governance
  • Security & Privacy
  • Infrastructure
  • Illuminate
  • Segments
  • Charts
  • Dashboards
  • Playbooks
  • Use Cases
  • Funnel Optimization
  • Product Adoption
  • User Behavior
  • Product Led Growth
  • Customer 360
  • SaaS
  • eCommerce
  • Financial Services
  • Why Heap
  • The Digital Insights Platform
  • How Heap Works
  • How Heap Compares
  • The Future of Insights
  • Resources
  • Blog
  • Content Library
  • Events
  • Topics
  • Heap University
  • Community
  • Professional Services
  • Company
  • About
  • Partners
  • Press
  • Careers
  • Customers
  • Support
  • Request Demo
  • Help Center
  • Contact Us
  • Pricing
  • Social
  • Twitter
  • Facebook
  • LinkedIn
  • YouTube

© 2023 Heap Inc. All Rights Reserved.

  • Legal
  • Privacy Policy
  • Status
  • Trust