Applications built with web technologies, something that was a curiosity a few short years ago, have emerged onto the scene as a must have for most organizations. Transcending websites and providing users with a more open and unbounded experience, web applications are everywhere. Likely the main reason you are reading this series is to determine how modern frameworks enable you to build web applications.

In previous posts in this series, we have discovered how our frameworks interact with us and how to put the basics together, but now it is time to really take in the whole album. In this post, we are going to explore how frameworks conceive of an application. That said, applications are never standalone. They will almost invariably need to get and send information and we may need them to work offline.

This is part of a series on web frameworks. If you’ve missed our previous posts, you may want to start with If we chose our JavaScript framework like we choose our music…

Approach

The term application can mean different things to different people and frameworks, so we need to understand what a framework considers to be an application. We will also explain the major architectural components of an application in the framework and if the framework enforces any particular application model.

State management

Applications have state, which changes as users interact with an application. Frameworks manage state in different ways. Some frameworks have very strong opinions about how to manage application state, while others focus more on creating the application interface and leave the how up to the developer.

Data integration and persistence

Most web applications will need to retrieve data from another system, often from the web server that hosts the application code. Retrieving business data from a server is a very common task for a web application. In addition to being able to retrieve the data, there is the need to send the data back to the server or even persist the data locally. We will review how the frameworks provide help for these common use cases.

Service integration and orchestration

While a well-designed service architecture should make it easy to create a front-end application, the reality often falls far short of that expectation. Whether trying to integrate calls to several back-end services, or facing off against a microservices architecture which requires the client to orchestrate calls to the various services, persisting data from one call to the next, our web applications end up doing more than might be ideal. Because of the rapid rate of change that a web application may accommodate, enterprises often put the application in the driver’s seat of managing the business process. If that ends up being the case, it is good to know what, if any, help the web framework can provide for us.

Offline

While Progressive Web Applications (PWA) define more of a set of good practices coupled with a few standards, one of the key requirements is offline capability. While the industry tends to focus on this feature for mobile, there are many use cases where not having reliable connectivity to the internet is an important application consideration. We will look at this particular aspect to see what these frameworks offer.

Jump to:

  • logo
  • logo
  • logo
  • logo
  • logo
  • logo

Angular 2+

Approach

Angular 2+ approaches applications with the main focus on two-way data binding between the component and the template. In Angular 2+ terms, a component controls a patch of the screen called a view. Angular 2+ templates are dynamic, and attributes and directives in the template augment the run-time behavior.

Angular 2+ has the concept of services, an abstract concept of any sort of class that provides some sort of service. Angular 2+ encourages abstracting out services from components but does not enforce this separation.

Angular 2+ heavily relies upon the concept of dependency injection, which is a run-time situation where instances of services are injected into instances of components. Services are registered with the Injector which then supplies the services to the components.

Angular 2+ uses its own overlay on the JavaScript module system, calling these modules ngModules. Angular 2+ also relies heavily upon metadata, injected into JavaScript classes, to augment the behavior of the Angular run-time libraries.

State management

Angular 2+ does not have a distinct concept of state management. It encourages a pattern whereby services are created and injected into components. Those services can be part of an application state or other data, like configuration information, data retrieval, etc. Angular 2+ focuses much more on authoring front-end components, described as templates, versus enforcing a higher-order concept of an application.

Data integration and persistence

Angular 2+ provides the ability to communicate with RESTful services via its @angular/common/http module. This module exports a HttpClientModule which allows users to integrate this into components or create services which can be injected into components.

Outside of this module, there are no official services that provide integration in other ways and no data abstraction API. There are several third-party components which provide services that interface with IndexedDB and LocalStorage as well as integrating with things like GraphQL.

Service integration and orchestration

There are no specific tools for service integration and orchestration within Angular 2+. These high-order concepts tend to be built as services, which, when offered up as modules, can be re-used in applications.

Offline

Angular 2+ considers offline to be a mobile feature. Angular 2+ previously provided out-of-the-box support for scaffolding a mobile application with offline capabilities via a service worker, but currently, the Angular CLI does not support offline and it is not clear when and if it will be supported again. The main landing page for the feature indicates that it is alpha but the instructions recommend using an outdated version of the Angular CLI and suggest using options that are not supported on the current version of the Angular CLI.

There are several tutorials available which provide some instructions on how to incorporate offline functionality into an Angular 2+ application.

React + Redux

Approach

Redux provides the APIs for what is traditionally considered an application, while React provides the user interface. React and Redux are designed to work together or independently.

Redux adheres to a strict unidirectional flow of data. A Redux store contains the application state where changes are applied to the state with reducers, which are pure functions that do not have side effects and contain no hidden state. Therefore, given X input to a pure function, you are guaranteed to get Y output. The Redux store is called with an action that identifies which reducer should be applied and what arguments are supplied to that reducer.

The architecture is designed to reduce the amount of cross-dependency between different parts of the application and to make it possible to separate parts of the application and make it easier to test every possible permutation in isolation, reducing the need for knowledge of other sections of the application.

React is then designed to deal with a state container like Redux. React splits the concept into two different types of components, presentational and container components. Presentational components are not aware of the Redux state container, they simply render the properties that are set on their instances and invoke callbacks that are set as part of their properties when appropriate. Container components are explicitly aware of the Redux state container, subscribe to changes of the Redux state and dispatch Redux actions. These could be considered more controller-like in the lexicon of MVC type application patterns.

State management

Redux focuses on state management of the application, using the dispatched actions to change the state of the application and container components to be connected to the application’s state store. This creates an entire reactive model that causes changes to the application state to be reflected in the presentation of the application.

Connecting presentational components to the application state is usually done through a container component which interacts with the application store. For example, if you had a Todo Item List like:

function TodoList({ items, onTodoClick }) {
  const todoItems = props.items.map((item) =>
    <li onClick={() => onTodoClick(item.id)}>{item}</li>
  );
return <ul>{todoItems}</ul>;

You would create a container component which would provide the visible todo items to the presentational component, along with the functionality needed to interact with the application store:

import { connect } from 'react-redux';
import TodoList from './TodoList';

function getVisibleTodos(todos, filter) {
  switch (filter) {
    case 'SHOW_ALL':
      return todos;
    case 'SHOW_COMPLETED':
      return todos.filter((t) => t.completed);
    case 'SHOW_ACTIVE':
      return todos.filter((t) => !t.completed);
  }
}

function mapStateToProps(state) {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  };
}

function mapDispatchToProps(dispatch) {
  return {
    onTodoClick(id) {
      dispatch(toggleTodo(id));
    }
  };
}

const VisibleTodoList = connect(
    mapStateToProps,
    mapDispatchToProps
)(TodoList);

Data integration and persistence

Redux’s simplistic approach to state management is nice but is limited by design. For example, actions must be objects that contain a type property. This can complicate business logic that may need to perform asynchronous actions such as fetching data from a server. This pattern of simply dispatching actions can be extended.

Middleware lets a user wrap the store’s dispatch method and add more functionality. This means that the value passed to dispatch can be a function instead of an action. That function will receive the dispatch method as an argument, which can be called from within the function after an asynchronous call, for example. The Redux middleware (thunk) can be applied using the applyMiddleware function from Redux.

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

const store = createStore(counter, applyMiddleware(thunk));

store.dispatch((dispatch) => {
    setTimeout(() => { dispatch({ type: 'DECREMENT' })}, 1000);
});

A dispatch action passed into the component can then make calls to a server, perform business logic, and then dispatch an action:

function mapStateToProps(state) {
    return {
        count: state.count
    };
}

function mapDispatchToProps(dispatch) {
    return {
        increment() {
            return dispatch({ type: 'INCREMENT' })
        },
        incrementAsync() {
            fetch('https://fetch-action.com')
                .then((response) => response.json())
                .then((action) => dispatch(action));
        }
    };
}

function App(props) {
    return <div>
        <h1>Hello, World!</h1>
        <Counter onIncrement={props.incrementAsync} count={props.count}/>
    </div>;
}

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(App);

There are several third-party libraries that abstract data integration further and provide abstractions that work well for interfacing with local persistent storage including IndexedDB.

Service integration and orchestration

Both React and Redux are quite focused on the specific problems they are trying to solve. They promote a more pure JavaScript approach to problems by using simple, assumed immutable data structures and rely on built-in language features and APIs, especially those found in ES6+. React and Redux might be used as a foundation for an application, but more complex business logic and interfacing with services would need to be provided, though this straightforward pattern generally makes it easy to integrate additional libraries and tools.

Offline

While neither Redux nor React provide an explicit offline capability, by containing all the application state in a single store, with the ability to insert middleware, it is easy to persist the state within the browser. Because all changes to the application state can be known, this enables the ability to add features like time travel where the changes to the application state can be stored to be replayed later, primarily for the purpose of debugging.

There are some third-party solutions that provide a more robust framework for creating and managing the Redux state store in an offline mode.

Because React + Redux lacks a specific framework, the biggest challenge comes from creating the right patterns in any container components that deal with modifying the state in an offline mode in a graceful way. This requires adopting a third-party solution or having the appropriate knowledge of how to ensure the state is kept in a consistent fashion and continues to be functional when disconnected.

Vue.js

Approach

Vue.js does not mandate a project or application structure. For applications, Vue.js provides a few key areas of functionality which are combined together to form an application:

  • Web Component like functionality
  • Reactive/observable JavaScript objects
  • An application state container with Vuex

The main focus of Vue.js is to provide a JavaScript-based, flexible, MVVM library, where Vue.js provides the ViewModel with two-way data bindings between JavaScript objects and the DOM view.

The vue-cli package can scaffold projects for a variety of purposes.

State management

For simple applications, the state of a Vue.js application will just be the values of all the data properties in its components. Components that need to share state can use shared objects or data stores, and a non-rendered component can be used as a global event bus.

For more complex scenarios, Vue.js provides a reactive state library inspired by Flux called Vuex. This library provides a central store for the entire application state and the means to update the state in a consistent and predictable manner. For large applications, Vuex allows a store to be separated into modules, where each module is essentially a self-contained store with its own state and mutation logic.

Data integration and persistence

Neither the core Vue.js library nor Vuex provides any direct support for data persistence. There is an official Vue.js library providing Firebase bindings, and a number of third-party libraries providing access to local storage, RESTful resources, and more.

Vue.js previously recommended vue-resource as the official HTTP client library for Vue.js, but the Vue.js team eventually concluded that this type of functionality was really outside the scope of Vue.js, and no longer recommend any particular solution.

Service integration and orchestration

Vue.js focuses on providing libraries to create applications. It does not provide any official pattern for integrating with services or orchestration of services within applications. Vue.js assumes that developers may express whatever logic they prefer within the ViewModel.

Offline

Vue.js does not offer any specific offline support. However, individual Vue.js component states may be serialized, as can the entire Vuex application store. By using other libraries, it would be easy to store the application state and restore it later.

Dojo 2

Approach

Dojo 2 provides functionality for creating full-featured web applications, including user interface components, routing, state management, code splitting, lazy loading, offline support, interfacing with external resources and robust testing.

Dojo 2 encourages the use of unidirectional data flow, with the application state being set as properties on a top-level widget which propagates those properties to additional widgets. Widgets generate higher-order events for user input in which a controller would modify the application properties/state and the widget system reacts to those property changes. Dojo 2 discourages storage of state within widgets/components and has introduced the concept of meta providers to manage transient application state on behalf of widgets, being able to intelligently invalidate widgets to cause them to re-render.

The scaffolding of applications is accomplished via the CLI command dojo create app.

State management

The @dojo/stores package provides a state management approach for Dojo 2 applications, making it easy to use with large data sets, and leveraging the Container/Injector to bind that data to widgets. Existing state management packages such as Redux may easily be used directly with Dojo 2, by creating a binding between the Redux store and an application widget’s properties, leveraging the @dojo/interop package.

Data integration and persistence

The @dojo/core package provides the request API which is very similar to the Fetch API to be able to interface with RESTful services in an isomorphic manner.

Dojo 2 provides a fully-featured state management package which supports the concept of resources to enable developers to describe a complex application data model.

Service integration and orchestration

Dojo 2 does not currently provide any specific tools to streamline service integration or orchestration. Dojo 2 being a flexible and open framework though means that it should be straightforward to integrate other packages that might provide this higher order orchestration. Also, as the concept of a Dojo 2 application develops, there may be more opinionated ways of managing service integration and orchestration.

Offline

Full Progressive Web App support is being integrated into Dojo 2, which provides support for Service Workers to make it easier to enable offline support. Dojo 2 provides a more structured approach for persisting the application state to make it easy to allow offline support.

Ember

Approach

Ember.js has a very structured approach to building applications which follows an MVC architecture approach. Every application in Ember is one that is an instance of a class that extends Ember.Application. This manages the state of the application and coordinates the flow of the application.

Logically, an Ember application defines routes, which are managed by the Router. When these routes are navigated to, they affect the model and navigate to a template. A template contains components which can act as a View or a Controller for a model.

Ember also relies heavily on the concept of dependency injection, where the application contains a registry of different classes that make up the application and can instantiate and inject instances into the application.

State management

An Ember application will have an application store which contains the single version of truth for the application. Controllers interact with the Models of the application to handle changes and respond to user input to affect the state of the application.

Data integration and persistence

Ember Data standardizes on a set of conventions called JSON-API to provide easy integration with back-end data and services. Effectively JSON-API is a set of conventions to create RESTful services without the need to debate the implementation details. Ember Data adheres to these conventions.

Service integration and orchestration

Ember.js applications have higher order concepts of Services and the Run Loop which provide a framework for creating, scheduling and managing queues of long-running processes. This provides a level of marshaling of work to provide efficient scheduling of business logic. Ember uses the Run Loop to provide some internal work management, but it is also designed for application developers to manage asynchronous workflow.

Offline

There is no offline solution provided directly by the Ember.js project. There are several third-party solutions which make it possible to persist the application store in an offline state, as well other tools which make it easy to integrate offline functionality.

Aurelia

Approach

Aurelia focuses on providing a structured MVVM application framework to power single page applications. At the same time, Aurelia focuses on being a modular system which does not strictly enforce it being used in an MVVM way.

Aurelia applications bootstrap themselves, with the convention of a main module which exports a function named configure() which operates on the application object to set up the application. Like some server-side frameworks, the concept of plugging in functionality to the application via middleware is present. Aurelia promotes a fairly decentralized approach from that perspective, where different Views, built of Templates/Components are bound to ViewModels, which in turn interact with the application’s Models, navigating through the single page application via Routes.

Like Angular 2+ feels familiar to those who used Angular 1, Aurelia will also feel similar. The Aurelia team worked heavily with Angular 1 and joined the Angular 2 team, but decided to start Aurelia after there were divergent views of the direction of the projects. Aurelia goes further in embracing modern JavaScript and TypeScript, striving to avoid divergent patterns from the underlying language.

State management

Aurelia was originally designed to be an MVVM framework, meaning that two-way data binding between the View and the View Model allowed the View Model to act on the applications Models, which contain state. As the pattern of container state has become popular, Aurelia appears to have tried to pivot and support the use of Redux for application state. In the end, Aurelia tries to not be too dogmatic its about approach and focuses on providing tools which can create feature-rich web applications.

Data integration and persistence

Aurelia provides an abstraction to the Fetch API as well as the HTTP Client/XHR API. These appear to be the limit of Aurelia’s official support, depending upon third-party packages and solutions to provide further access to data.

Service integration and orchestration

As mentioned above, there are abstractions for Fetch and XHR, which would allow a developer to interact with services on a server, but there are no higher order concepts that are provided out-of-the-box.

Offline

There is a module for caching, but outside of this module, Aurelia does not offer an out of the box solution to supporting offline applications. There are a few open issues in the Aurelia repositories discussing support for Progressive Web Applications, though they seem to be focused on scaffolding a project, versus providing any higher order functionality to manage the application via service workers in an offline context.

Summary

Angular 2+

Angular 2+ does not provide a strict application model. It is possible to create a more formal application structure, like MVC, MVP, or MVVM on top of Angular 2+. It does rely on several very specific Angular ways of development that might make it awkward to integrate with other frameworks or libraries.

Most of the logic of the application is left to the end developer, with Angular 2+ focusing more on facilitating the tying of logic to components that are part of bigger views, which is very reminiscent of the form and control application model, where the user is provided forms with controls which interact with and create a directive application flow.

React + Redux

React + Redux are two tools that help build applications that heavily rely upon modern JavaScript constructs. Each tool is specifically designed to do their main purpose well and not much more. This is best suited for situations where there is a lot of knowledge in the development team of how to build web applications. When that knowledge is available, very effective applications can be built rapidly.

If there is not sufficient knowledge and technical leadership, React + Redux applications can quickly become unmaintainable and overly complex and fail to meet their business objectives.

Vue.js

Vue.js focuses on providing the ViewModel of an MVVM application. Adding Vuex provides an application state container similar to Redux. If you want to follow the pattern of MVVM, or you want to incrementally re-wire an existing web application to a more modern architecture, then Vue.js is likely to work for you.

While Vue.js focuses on providing a few key features, instead of an entire framework, it does not feel as open-ended as React + Redux, meaning that it can be safer to use with teams with less knowledge and technical leadership than is required with React + Redux.

Dojo 2

Dojo 2 provides a front-end to an application, which is flexible enough to be integrated with other tools to create a whole application. If you are looking for a more structured and opinionated reactive style solution, then Dojo 2 is worth consideration.

Ember.js

Ember.js adheres to a structured MVC model. It is strongly opinionated and has a breadth of concepts beyond application state management, to deal with an application as a holistic concern. Ember provides extensive documentation and a right way to architect and build Ember applications. If you want a framework for building full web applications, where patterns and anti-patterns are clearly defined and the MVC application model is aligned with your architecture, then Ember could be a strong candidate for you.

The risk is that there are higher order APIs that are built on top of the standards, meaning that Ember applications will likely stay as Ember applications and changing libraries or approaches is likely to mean wholesale re-writes down the road. This is, of course, a risk with any library chosen, but the breadth of functionality of Ember is more likely to lead to framework lock-in.

Aurelia

Aurelia was designed to try to support an MVVM application model, though it does not strictly enforce it. Aurelia is a flexible framework that will feel very familiar to users of Angular 1, with attempts to fully leverage modern JavaScript and TypeScript instead of introducing patterns that are specific to the framework. In theory, this makes Aurelia easier to integrate third-party solutions but also migrate to other patterns in the future.

Because Aurelia applications are easy to extend, it can be quite easy to end up with a sprawling application that has grown organically over time. Teams need to ensure that they establish conventions and patterns up front to help ensure that the application continues to be maintainable over the long term. Because Aurelia is flexible, it would be quite easy to end up with an application that is vast and complex and inconsistent in the way it operates.

Up next

Now that we have explored the core of what we are likely looking for in a framework, in the next post we will be exploring the situations for which each framework is best suited, and see if they have any magic up their sleeves for those use cases. We will find that while some bands are multi-genre and hard to pin down, some might only be good at playing one tune.