I find that we write a lot of the same code for components that just fetch data and render the result. This also sounds like a trivial job, but in reality, keeping the client state in sync with the server state has a couple of catches and things can get more complicated than expected.
That's why I was thinking about how we can simplify and unify this process. Meanwhile, I also want to make the pit of success wider, and therefore this solution should be robust and include helpful utilities.
The idea takes inspiration from react-query, swr, and multiple routes implementations from SvelteKit and other frameworks built on top of React. I call it ngx-query-state.
The main idea is to make working with HTTP requests easier and to provide a better experience to the users. From the technical side, the component needs to configure how it wants to make HTTP requests, the rest are implementations details, which are abstracted in ngx-query-state.
Let's take a look at the most basic example, a component that displays the result of an HTTP request.
The component provides the service in which it makes an HTTP request with
provideQueryState and as a result, the
QueryState instance is injected into the component.
QueryState, we can access the HTTP response with the
data$ Observable. This is used in the example below to render the collection of customers.
While the above is going to render the customers, it doesn't take into account that the request can fail. Nor, does it has any indication that a request is pending and waiting for its result. When this happens, the user just gets to see an empty screen.
We can write a more robust implementation to provide a better experience.
ngx-query-state provides a
query-state-template component and the
qsError directives. Based on the state of the request, the corresponding template is rendered. This way, we can represent the current state to the user.
Though, this has a few drawbacks. This can get repetitive, the template looks bloated, and it's also hard to create the same behavior across multiple components. To keep this as simple as possible, you can implement your own loading and error templates, which are used as the default templates but can still be overwritten in specific cases.
For example, a custom loading component looks like this.
This component can then be configured as the default loading component.
The same can be done for the error component.
To do this, provide your component to the
With the default components set, the components that render the data can be refactored, leaving just the essentials.
Until now, we haven't seen the service that invokes the HTTP request.
This service is just your normal HTTP wrapper service but it also implements the
QueryService interface, which has the
query method that gets invoked from the
The basics are covered, so let's add a search parameter to customers' query.
To implement this, add an input control to the HTML template and pass the
valueChanges Observable to the
queryState.update method. Now, when the input changes the query method is re-invoked with the updated search parameters.
As a side-effect, the URL is also updated and includes all the search parameters.
I find this useful because it creates shareable links, and makes it easier to take a look at the trace logs.
Because the search includes a name parameter the query service also needs to be updated.
query method receives the
QueryParams, which include the URL params and query params.
Most applications also contain screens to modify an entity.
For this, the service is re-exposed via the
Via the re-exposed service, it becomes possible to invoke the methods of the service.
Because these methods mostly return observables, there's the
effect method to automatically handle subscriptions.
When the component is destroyed, all subscriptions are also cleaned up.
With the re-exposed service in combination with the
effect method, we can implement side-effects, e.g. updating an entity.
We've seen the most important helpers that ngx-query-state offers. Besides these helpers, ngx-query-state also includes the following features (that can be configured):
- ✅ Caching - render the cached version while it's refreshed in the background
- ✅ Retry - automatically retry a failed query 3 times with exponential backoff
- ✅ Revalidation - revalidate the current version on time intervals, screen focus, online detection
Consider this as a proof of concept, and feel free to try out ngx-query-state in your own projects, or in the examples included in the repository.
Use the following command to install ngx-query-state to your project.
I appreciate it if you would support me if have you enjoyed this post and found it useful, thank you in advance.