Keeping client and server in sync can be tough when building a single page app, and involves tons of boilerplate code. In this write up we’ll combine the best of two worlds by hooking up Angular 2 with Relay, making your workday much more fun and productive!
This technology is so promising, that we’re running an internal experiment at Small Improvements to evaluate its benefits. We gave a talk about Angular 2 and Relay at NG NL 2016 and this is a writeup of the content. (The video of the talk will be published within the next weeks.)
When you’re building web apps you’ll mostly have to deal with client-server communication. Relay is a library that simplifies client-server communication on an abstract level. In this post we’ll use an example and three challenges to demonstrate the power and simplicity of Relay.
A conference planner app will serve us as the example. This app displays a list of conferences. A user can can attend or leave conferences.
The UI looks like this:
Challenges with client-server communication
The three challenges we will look at in detail are:
- How to define data requirements explicitly within our components?
- How to ensure a consistent state after changes?
- How to ensure that we don’t break the contract between client and server?
Challenge #1 How to define data requirements explicitly within our components?
When writing a single page web application with or without a framework, such as Angular or React, you’ll follow best practices and write UI components. Responsibilities of these components are to display and modify data.
The component is usually connected to a layer that provides data. This could be a standard service pattern, a flux like implementation, such as Redux or Reflux, or an RxJS stream. The implementation doesn’t matter, the point is that the component needs to rely on the fact that data it needs is provided.
In the graphic below the component asks the ServiceLayer for data, the ServiceLayer asks the CommunicationLayer, which requests the data from a Backend. The data is then piped through until it reaches our component.
The problem here is that the data needs of a component are not specified where they are used. It is the component, which knows all the information about what data it needs, yet it has to rely on someone else that the data will be requested accordingly. Or in other words, the usage is completely disconnected from the request of the data
This disconnection results in components that are hard to understand and tricky to reuse. It’s not obvious what data is needed from the server and if you want to reuse the component you’ll have to ensure that it is properly wired to the layer that provides the data.
Challenge #2 How to ensure a consistent state after changes?
A common and very tricky challenge is to keep the app state consistent. Every time a user interacts with the application, its state is changed. These changes affect the server state and our app state. To ensure consistency we’ll have to update the server and make sure that our internal app state is updated accordingly.
These interactions often have multiple side effects, which can make state updates very complex. This results in complicated manual updating or re-fetching of data.
Let’s use the conference planner as a concrete example. We, as the user Lisa, want to attend NG-Europe. To do so, we’re clicking on the “Attend” button and we’d expect to see the following changes in the UI (highlighted in orange):
- The Button label changes from Attend to Leave and its color from green to red.
- The attendance count for NG-Europe is increased by one.
- NG-Europe is added to the list of conferences, which the user Lisa is attending.
This single action triggers side effects on multiple data nodes, that would need manual updating or re-fetching.
Challenge #3 How to ensure that we don’t break the contract between client and server?
APIs are usually constantly evolving and most of the time it’s a pain to ensure, that you don’t break consuming clients. A fallback solution is to apply versioning of your API or the attemot to stay backwards-compatible at all times, but this makes even simple changes like renaming a field very inconvenient.
The agreement or contract between client and server is very implicit. Every time a component is using a specific property (for example the date for a conference) it couples itself to server – the contract gets richer. The client expects implicitly that the conference contains such a property. If we now rename or even delete this property on the server side, w don’t have a simply way to check if we broke the contract – we might only discover the issue through a runtime exception. This can lead to developers being very hesitant to refactor the server side API at all, which is far from ideal.
Relay is a client library which depends on a GraphQL backend. This means Relay can’t talk to a REST/JSON-RPC server, only to a server which is able to understand GraphQL requests.
This is the general architecture:
The client includes the Relay library which sends GraphQL requests to the server. The server uses the GraphQL library to execute the query and retrieve the data. You can wire the GraphQL layer on top of your business layer, as you would with a REST implementation. It is not designed to solve graph-like problems, but to provide a unified query language to request graph-like structures.
Here is what a GraphQL request and the corresponding response looks like:
The so called “fragment” specifies properties on an entity. In this example we’re querying for the properties id, firstName and lastName of the type User. The response is a JSON representation of the requested properties. GraphQL is designedt to return only data that was specifically asked for, never less and never more.
Relay follows the principal of having Presentational and Container Components. The container components shield away the request logic from the UI components. Every component has a RelayContainer counterpart in order to get the data it needs. The component declares its data requirements and the RelayContainer is responsible to provide this data.
Relay and Angular 2?
Relay, in the current release, is only available together with React. But the fundamental ideas and principles are in fact totally independent of React. At Small Improvements we have a very large Angular application, that’s why we wanted to use Relay with Angular 1 & 2. The solution Relay provides made us very curious and we wanted to test if Relay can keep its promises.
This motivated us to explore the possibilities of integrating Relay with Angular and Angular 2 and we did the following:
- Creating a modified version of Relay, that doesn’t depend on React, but instead can be used framework-independent: Generic-Relay
- Connecting Angular 2 and Relay by writing an annotation (@connectRelay), to pair Generic-Relay and Angular 2: Angular2-Relay
Mechanism of the @connectRelay annotation
The @connectRelay annotation adds two @Input Fields (relayProps and route) to an Angular 2 component. The ngOnChanges callback is used to inform the RelayContainer about any changes in regards to these two inputs. The RelayContainer binds the requested data to the component’s controller via the relayData property.
Using Relay with Angular 2
Now that you have a brief idea of the functional principle, let’s look at a simple code example. We want to wire an Angular 2 component, that displays a user, to Relay.
After initializing the RelayContainer “UserAccountContainer”, we declare all data requirements in the fragment.
The @connectRelay annotation makes sure that our plain Angular 2 component is connected to the RelayContainer. After the magic of the annotation is initialized, Relay will ensure, that we can access the requested data in the relayData binding.
Solving the Challenges
Challenge #1: How to define data requirements explicitly within our components?
Relay allows us to declare the data needs of a component within the RelayContainer’s fragment.
For example this is how we’d specify the data needed to display a conference in our conference planner example:
Challenge #2: How to ensure a consistent state after changes?
Relay manages a central store, that acts as a single source of truth. By specifying in a declarative way what state changes are to be expected after an user interaction, Relay can handle the all side effects of this change. This includes updating all components (via the the RelayContainer). The definition of all side effects is specified in GraphQL.
Changes to the state (of server and client) are expressed with Mutations. A Mutation defines what changes will result of an action. We also specify all data that is sent to the server.
Here is a snippet of the Mutation to let a user attend a conference:
In getFatQuery() we declare all data we expect to change and therefore has to be requested by Relay.
In getConfigs() we tell Relay the types of changes we want to perform. In this example we want to change some fields (“FIELDS_CHANGE”) and update the list of attending conferences (“RANGE_ADD”). (The complete Mutation can be found here)
Mutations are the most complex part of Relay and will probably be improved in the near future.
The takeaway here is that we declare all side effects of an action and let Relay take care of updating the server and its internal store. Relay also will update all components for which any data has changed.
Challenge #3: How to ensure that we don’t break the contract between client and server?
As mentioned before, GraphQL is statically typed. This feature enables us to verify at build time if the contract between client and server is upheld. The GraphQL server provides a schema file which is used to compile the client’s fragments. That way we ensure that neither client nor server violates the schema.
If we were to query for an invalid property, e.g. the rating property on the Conference type, our build would fail:
This way we avoid any unexpected runtime exceptions by checking the contract as early as possible.
Relay and GraphQL help us to simplify client-sever communication by:
- Providing a declarative way to specify the data needs at component level
- Managing a central store including a declarative way to specify changes/updates
- Having a statically typed schema, which serves as contract between client and server
The most fun point: As demonstrated by the Conference Planner example, it is possible to use Relay with Angular 2!
Finally we’d like to thank NG-NL for organizing a great conference and for having us.
We hope this writeup gives you a clear understanding of how Relay simplifies client-server communication and how powerful it is to use it with Angular 2. If you have any questions or feedback, feel free to contact use via Twitter (@AndiMarek & @SFroestl) or leave a comment here.
Angular2 Relay (including the Conference Planner example)
List of Relay/GraphQL resources (including different GraphQL implementation)
We don’t say that Relay is the answer to everything and we didn’t discuss when to use Relay. Of course Relay is no silver-bullet and as always there are tradeoffs to consider. Our point is to show that it is possible to use Relay with Angular 2 in a very nice way.
This is mainly because Relay itself is nearly generic out of the box, since it’s React dependencies are minimal. In fact the Generic-Relay project will hopefully be deprecated in the future, because Relay itself will get a core, that provides similar capabilities (See this Issue)