Since the Server π is doing it, let's make sure the Browser π doesn't.
The Problem
With Angular Universal properly setup, we start having an issue. We have a relatively complex feature that let's say does this:
- Fetches all movies the user tracked so far
- Shows a pretty UI with all these movies
- Allows the user to filter this list by year, genre, director and actor
This seems like a common scenario. The problem with it is that this work will be done twice: once on the Server and once on the Browser. This means double the amount of network requests and a flash of content when the Browser finishes wiring up Angular.
Good to know before jumping in
Angular has something called TransferState Service. Long story short, this is a service you can use to send data between the server and the client in a key-value way. If you want me to cover this in more depth, please let me know.
This examples also uses Akita Store, but any stream-service solution(NgRX, ServiceStore or a simple BehaviourSubject) would work. Think of it as a place to put data, and to react to data changes(I know, this is already getting complicated π ).
Solution
There are three parts to the solution:
- When the Server is done putting data in the Store, put that data in the StateTransferService as well.
- On the Browser, if there's data in the StateTransferService, use that data to boot up the Store, instead of the default value.
- If there was data in the StateTransferService, do not emit after booting up the store, this is needed in order to avoid a flash of content.
Part 1 - Put data in the StateTransferService
For simplicity, let's assume you have a service called UserMoviesService, and this service has an aptly named method called setup()
. This method is called from the Container Component and might look something like this:
Now, if this look scary, don't worry. Let's unpack π¦ it.
First, in the setup
method we listen to the userSelection$
stream from the Store. This means that every time the user changes their selection(e.g. only wants to see the tracked movies older than 2010), this stream will emit a new value(a new selection).
Based on the selection, we do a new HTTP call where we fetch the new data. That data is later manipulated and added to the Store(maybe combined with other streams).
The last tap
is a key one. There, we check if the code is running on the Server. If it is, we get the Store value and put it in our StateTransferService
.
Hope it makes more sense now.
Part 2 - Boot up the Store with the correct value
The second piece of the puzzle happens when the Store is booted up. Here's an example implementation:
Yet another piece of code that looks complicated, but hopefully we can decipher it π. First, we define an interface for how the SearchMoviesState looks like. This represents the shape of data held by the Store. Afterwards, we define a method that returns the seed data for the Store. This is the key part. In here, we check if there's a value in the StateTransferService. If there is one, we use that, otherwise we use the default seed object. Lastly, the boilerplate part for creating the Store. The only difference is the fact that we need to inject the StateTransferService.
Just a bit more, we're almost there.
Part 3 - Avoid Flash of content for pre-rendered pages πΈ
We still have some flash of content though. This is because when the setup happens on the Browser, the stream will have an initial emission, which will trigger a page re-render because even though the objects have the same data, they are different objects(different references). Here's how we can get rid of that:
It's almost the same code as on step 1, but we added an extra piece of logic. In the switchMap
we now check if there's already a value in the Store and if there is, we clear it, and we return NEVER
. This stops the stream basically. We're not going to do another network request, and we're not going to update the Store.
With all of this in place, when the Container Component calls the setup
method, the store will be initiated and wired up, but it won't emit a new value, so we get to keep the DOM rendered by the Server.
And this is how you get the best of both worlds: Server Side Rendered Speed + SPA functionality, without doubling the effort.
Want more?
If you want to know more about this, or something doesn't make any sense, please let me know.
Also, if you have questions, you know where to find me⦠On the internet!
Be kind to each other! π§‘
Over and out
Finally, I wrote a new blog post βοΈ => Efficient Content Hydration with Angular Universal https://catalincodes.com/posts/efficient-content-hydration-with-angular-universal #Angular #AngularUniversal #ServerSideRendering