In this page, we will be reviewing some topics regarding use of the RxJS library. If you're not familiar with this library, it provides functionality for working with the publisher/subscriber pattern via Observables, Subjects, and functional pipes. If you would like to learn more about the core concepts, please visit our Introduction to Observables with RxJS.
One of the most common and repetitive tasks when writing a website or web app is to make some network call to retrieve data, then store and/or pipe it to the user interface. In some cases, the data needs to be retrieved fresh from the service every time it is needed, in others, it can be cached in the UI for some time. Here, we will describe a simple and reusable approach to lazy loading data the first time it is needed, then caching the result afterwords. There are multiple ways of accomplishing this task, including:
- Using a state management framework to intercept the query with an effect.
- Upon each invokation, manually checking some local value to see if its defined in the consumer, then calling the load function if not from the service.
- Using some framework or custom implementation to decorate the function.
The approaches above, and others, can be valid if done appropriately, but there are many instances of these concepts being applied incorrectly, causing the codebase to be much less clean than it would have been otherwise, which is the opposite of the intended goal. The approach we will discuss is a cross between decorating the method which loads the data and caching it.
Lazy Loading within an Observable
The approach commonly used on www.bytethisstore.com is to create a subject/observable pair to hold and expose the data. The first time the observable is triggered, it will fetch the data and save it to the subject. Subsequent calls will directly deliver the data.
The benefits to this approach are:
- The network call and caching is encapsulated in one place.
- Consumers do not need to know anything about how the data is loaded or whether or not it is loaded when it subscribes.
- The implementation is concise.
- It is simple to add additional logic for caching rules, such as duration and expiry.
Adding Caching Clearing and Rules
Clearing this cache is as simple as setting the behavior subject's value back to null. A public method can be exposed to allow consumers to force a refresh.
More advanced cache clearing mechanisms can be accomplished in a variety of ways, such as:
- setting timers to call the clearCache method after some interval.
- adding an additional pipe to the behavior subject to clear the cache based on some response value.
- listening to some event to clear the cache.
The following is an example of a method to clear the cache based on the backend response:
Preventing Duplicate Invocations
When taking the approach above, one additional step is required to ensure that we do not make any subsequent calls to the backendService.getData method. There are a few ways of accomplishing this:
- Create some logic within the observable getter property to check if a backend call is already in progress. If so, don't make another one. This is a relatively simple approach but will require each consumer to maintain their own state.
- If you have access to the service itself, you can create some logic there, similar to the point above. This will not require consumers to maintain their own state.
- Decorate the service call method/function with a function which will collect subsequent invocations, hold them, and return the result from the first call to all invocations.
In our case for this website, we generally take the last approach by using a decorator from our @byte-this/funscript library. This is a lighweight library we've created and use ourselves which provides implementations for common functional patterns. An example implementation using this library is as follows:
The article below introduces our library with greater detail: