GraphQL and Angular: using the right option for managing remote and local state
GraphQL is growing rapidly since open-sourcing it by Facebook in 2015. The possibility that opened with GraphQL was to provide server data in the right shape for the UI. Thus, it minimizes the need for a state management like Redux. You don't have to write reducers, selectors, actions etc. anymore. The same is true for the data. The nature of the graph allows to go in depth and add all the needed related objects to the response. A good explanation of the topic can be read in the blog post "How GraphQL Replaces Redux" by Mark Johnson).
Local state management can still help in some complex applications like managing drafts and forms, pagination or filtering settings. This brings up a question: "How can we handle the local state while using Apollo GraphQL?". Should we use the built-in Apollo cache for local state handling or do we keep NgRx as used in feedbackr? Maybe we should give a chance to Loona, a thin wrapper above Apollo? If they don’t fit should we use a lightweight state handling library like Ngxs or Akita? This blog post will provide an overview of some possible solutions.
Loona is an open source state management library created by The Guild. It was built on top of Apollo to keep the local data in one store and improve the developers experience. They used the logic of actions and effects from NgRx to enable better separation between executing a mutation and updating the store.
To enable typed queries and mutations you first have to create boilerplate code in Loona. For every query or mutation, an additional step of converting it to Typescript interface is required. The advantages of the GraphQL interface do not outperform the overhead. As Loona is a fairly new open source project, it is not used by a lot of living projects. Unfortunately there hasn’t been any activity on the repository in recent months and it looks like The Guild has taken a step back. They haven't said that they will quit supporting Loona but they haven’t said they will continue with it either.
Apollo's cache allows you to store local data using GraphQL. You can have both local and remote data in the store and more importantly you can fetch it in the same query. The biggest benefit of using Apollo's cache as a local store is that it becomes a single source of truth.
Unfortunately, for using the local store you need to introduce some overhead in writing query and mutation definitions. Also, you cannot use the benefits of the strongly typed mechanism of Typescript anymore since a query to the local store has to be written as a GraphQL-Literal-String, the one like graphql-tag library provides.
There is a possibility to write a GraphQL schema on the client side, but this schema will not be used for validation but only for introspection in DevTools. Apollo may not be the best choice if you are using a lot of primitive values but it might help in situations where complex data structures need to be stored locally.
If Loona did not convince you and writing GraphQL for managing local state is not something that you are satisfied with, trying one of the redux-based solutions might be the right answer. It provides a more “natural” handling of the state. At the same time synchronisation between Apollo and redux-based store are required. In the next section we are going to present NgRx, Ngxs and Akita.
NgRx was the first Redux implementation available for Angular. Developer support for Redux Devtools plugin offers a list of latest actions with their impact on the state. It’s possible to jump to a specific action and even skip some. Developers can also dispatch an action directly from the DevTools.
NgRx handles changes of the state using observables that enable the chaining of multiple selectors. With Effects you can execute async actions and isolate them from the UI. One drawback of the NgRx is that it still needs you to write a lot of boilerplate. As claimed by the authors, it wasn’t created with a mindset of writing the code in the quickest way and the learning curve is often steep. The NgRx community is large and it continues to grow but the framework is strictly coupled with Angular.
The state management library Ngxs is only available for Angular. The support for Redux DevTools doesn’t provide all functionalities like NgRx. It’s not possible to jump to specific actions or skip some. Also, it’s not possible to dispatch new ones using DevTools. Ngxs uses functions inside the State class. These functions can be chained but it’s not structured as well as in NgRx.
Nevertheless, Ngxs has a lot of built in features including offline persistence, snapshot selection, forms synchronization, WebSockets and more. As the name says, the focus was on minimising NgRx boilerplate that is simply too big for some use cases. Support for DevTools may be worse, but boilerplate reduction, along with plenty of features and a healthy community around the library balance these drawbacks.
Akita is supported and maintained by Datorama and can be used with Angular, React, Vie, Svelte or even vanilla JS. As Akita was built on top of RxJS, the functionality in the DevTools is limited. Actions and their impact are viewable and you can skip to the specific action in the timeline, but it doesn’t support live skipping or dispatching in the DevTools.
Akita encourages simplicity by using object-oriented principles. There are no asynchronous actions as the subscription to the stream of data must be handled manually. However, the amount of boilerplate code is low, the biggest advantage compared to the opposing solutions. Documentation is fairly nice and Datorama’s developers often write blog posts about using it in different applications.
The community isn’t as big and built in features aren’t as plenty as in NgRx and Ngxs but the benefit lies in its simplicity and low amount of boilerplate. That in our opinion is something that cannot be ignored. For that reason we opted to use this library in our projects.
There are different options when trying to implement state management. We simply wanted to present an overview of the most promising ones. For our use case Akita was the most suitable solution because it introduces very little boilerplate and it's learning curve is faster than the others. It’s support is stable which is important for long term development.
Akita is also not framework dependent, but this doesn’t make it the best solution for every project. All in all, every option has its advantages and disadvantages, so in the end you need to decide on the best one for your use case.
Do you need help in deciding what to use for local state management?