This month we celebrated the public rollout of Bronto’s completely rebuilt reporting and analytics application and infrastructure. In addition to
building a state-of-the-art event and metric processing pipeline with Spark, we’ve delivered a brand-spanking new and highly reactive user interface for visualizing marketing performance reports. The front end technology choices and architectural decisions we’ve made have sped development, allowing us to increment and deliver new features fast as well as support a simpler mental model about how data and user actions update our application.
Bronto’s reporting pipeline maintains up-to-date metrics with online event stream processing, so we knew we needed a fast, reactive UI that could update quickly and cleanly. In addition, an early review of the customer facing features revealed an application rich with data (metrics of various events), user configuration (numerous forms of filtering and customization of reports ), and dynamic UI building (user-customized dashboards ). We embarked on a journey to build pluggable components, with self-describing data requirements, that could be reused across pages and dashboards with minimal context.
Our UI needed to respond quickly to state and data changes that potentially affect all components at once. Enter
React. React’s document object model (DOM) diffing frees us from having to worry about re-rendering components manually. If the properties (or props in React lingo) we’re passing to a component change, the component updates. If the props stay the same, React avoids costly browser display updates. And, due to React’s modeling of components as functions, we're able to embrace functional composition for
higher order behavior. Furthermore, within a large application, keeping track of user interactions and their effect on application state can get daunting. Enter
Redux. Storing application state with Redux (like user selections and cached data requests) allows us to write pure function components that only depend on data and callback functions passed to our components as props, making them simpler to reason about. We make use of an immutable state tree for powering efficient memoizations and optimizations and avoiding bug-tastic state change side effects.
Making it Smart
Bronto’s reporting UI utilizes
Higher Order Components (or HoCs) that wrap content rendering components with a container layer capable of fetching data and communicating fetch lifecycle changes (e.g., initial fetch, fetching updates, ready). These data fetching containers, in our architecture, declare their requirements as two functions: a function of props that returns values (variables) that we plug in to a function of variables (requirements) which return objects that describe the set of metrics needed by the component. Our requirements function outputs an object whose values specify a metric for which to resolve data - and whose keys represent the name at which the resolved data should be passed in props to the composed component. The sequencing of variables (as a function of props) (as a function of variables) and the favoring of simple values in variables support optimization of containers, such that data requirements will not be re-evaluated unnecessarily. This keeps us from updating the displayed currency code, for example, until the new metrics for that currency have been resolved. Now, you might be wondering what our metrics.orderCount function represents, given how our HoCs declare their data requirements. While our analytics event processing pipeline maintains a large set of metrics, we support feature flexibility by calculating some combinations of metrics on the client. For example, we can fetch a number of event counts and revenue numbers via API queries, but we derive ratios and complex metrics by performing those computations on demand and client-side . Our container engine understands this metric library, and the descriptors these functions produce, to decide what API queries are needed for a metric (or may be resolved from our data cache in the Redux store ) and what computations are needed on the client to produce a proper display value . As you can imagine, optimization of our containers is key here. Even when resolving metric values from our cache in the Redux store, our containers need to do a lot of work. To that effect, it’s important that our containers avoid unnecessary data updates (by optimizing on changes to variables) and memorize the result of metric "reduce" operations (for client-side computation of metrics) with a library named
This architecture, the declarative nature of our containers, the parameter-driven metric descriptors, and a Redux-action-based data cache, have a few key benefits:
We've enjoyed developing this architecture and learning from the React community. And, even though patterns for React applications are still being invented, we're confident that our approach will continue to pay dividends, supporting the feature flexibility we're so committed to provide to Bronto customers. Many thanks to Zach Bradshaw, who contributed to the research and writing of this article, and Mark Smith, who provided a peer review.
- It’s easier for us to unit test pluggable containers and assert that they’re correctly declaring their needs with respect to props.
- We can develop widgets that are plug-and-play ready for dashboards.
- We have feature flexibility, to define and derive new metrics in interesting and novel ways.
- We can manage cross cutting concerns like cache invalidation and refreshing (and someday, real-time web sockets!)