Beyond the Hype: The Realities of Angular SSR
Server side rendering continues to gain traction across the Angular ecosystem. The official documentation paints a picture of simplicity — run a single command, mind a few details, and everything works. In practice, the journey is far more complex. This article dives into specific zone-related challenges and issues that developers frequently encounter. Finding solutions online can be surprisingly difficult, and the fixes are rarely straightforward.
This first installment tackles two significant hurdles: dealing with the window object during the build phase, and implementing guards to restrict server-side access — particularly when authorization data is unavailable. The follow-up will explore initial state management, eliminating duplicated animation loads, sharing data between client and server instances, handling CSS driven by code (such as media queries), and working with the request object. Let's begin.
"I Mocked window, So Why Isn't It Working?" — Build-Time Complications
The server environment lacks a browser, meaning objects like window simply don't exist. That's an obvious statement, yet it's easy to overlook. The real trouble emerges with third-party libraries that depend on browser globals — libraries like Hammer.js or animate-css-grid — which resist simple mock solutions.
A Practical Example
Consider a straightforward service that configures Hammer:
import { Injectable } from '@angular/core';
import { HammerGestureConfig } from '@angular/platform-browser';
import * as Hammer from 'hammerjs';
@Injectable()
export class HammerConfig extends HammerGestureConfig{
overrides = {
swipe: { direction: Hammer.DIRECTION_ALL }
};
}
Nothing in that code references window directly, so what could possibly fail? You kick off the build, and the terminal outputs:
(window, document, 'Hammer');
^
ReferenceError: window is not defined
Your initial reaction is obvious — the window object is missing, and you forgot to add a mock. You check server.ts, and the configuration looks correct; domino neatly patches the missing window. Searching Google yields few answers that don't require hacking webpack or modifying the library's source. Fortunately, there is a workable fix:
@Injectable()
export class HammerConfig extends HammerGestureConfig {
hammer: any;
overrides = {
swipe: { direction: 30 }
};
constructor() {
super();
this.getHammer().then(_=>console.log('Hammer ready'));
}
async getHammer(): Promise<any> {
if (typeof window !== undefined) {
// conditional include because window is undefined on build
this.hammer = await import(
/* webpackPrefetch: true */
'hammerjs'
);
}
}
This approach enables on-demand loading of the problematic libraries. The code checks whether window is available in the current environment, then loads the library accordingly. The pattern looks simple, but it introduces its own quirks. If you're interested in the broader application of this technique, Netanel Basal's article covers how dynamic imports can lazy load components and explains the purpose of the comment shown above.
"How Do I Know if I'm Authorized?" — Guarding Routes and Bypassing SSR
Another frequent issue involves excluding certain routes from server-side rendering altogether. Imagine an area of the application that requires authentication but also depends on browser-only APIs, like storage. Running such a component on the server is impossible. Several strategies exist to work around this, though none are flawless.
The first method involves server configuration — intercepting requests for specific paths and returning index.html directly:
app.get('/auth-path/**', (req, res) => { res.sendFile(join(DIST_FOLDER, 'browser', 'index.html')); });
In my experience, this approach fell apart quickly. Every asset path inside index.html must be absolute, which either forces edits to that file or requires playing around with the rendering settings.
A second strategy is to maintain two separate application builds — one wired for SSR, another served as a traditional client-side app — then route traffic based on the path. This solution is clunky at best and outright messy at worst.
Both options face an additional complication when your app supports routing translation. Each translated route would need its own redirection rule. Multiply that by the number of supported languages, and both approaches become increasingly unappealing.
There is a simpler, less invasive workaround that I've found useful, though it carries a noticeable downside I'll address later. The trick: instead of skipping SSR, get the user to a temporary page, and once the application boots in the browser, that page triggers the real redirect.
Here's how to set it up:
1. Define a temporary component that acts as a placeholder the user sees while the app initializes — call it SsrRedirectComponent:
@Component({
selector: 'app-ssr-redirect',
template: `<h1>Waiting for initialization!</h1>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SsrRedirectComponent{
}
2. Implement a guard that fires only on the server. Its job is to request a redirect to a predetermined route:
@Injectable({
providedIn: 'root'
})
export class SsrRedirectGuard implements CanActivate {
constructor(@Inject(PLATFORM_ID) private readonly platformId: unknown, private readonly router: Router) {
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree {
if (isPlatformServer(this.platformId)) {
return this.router.parseUrl(`/${ROUTE_SLUGS.ssrAuth}`);
}
return true;
}
}
And the relevant part of App.-routing.module:
const routes: Routes = [...
{ path: ROUTE_SLUGS.ssrAuth, component: SsrRedirectComponent }
...]
3. The server-side rendering process follows the guard and serves the temporary component. However, this redirection stays server-side, meaning the actual URL in the browser doesn't change. When the client app loads, the router takes over, processes the current URL, and ultimately lands the user on their intended page.
const routes: Routes = [...
{ path: ROUTE_SLUGS.ssrAuth, component: SsrRedirectComponent },
{ path: ROUTE_SLUGS.protectedPath, component: ProtectedComponent, canActivate:[SsrRedirectGuard, AuthGuard]}
...]
On that temporary page, you can easily show a loading indicator as a stand-in. Users won't even realize they're in a holding pattern; the address bar already displays their destination, so the app working in the background goes unnoticed. At that point, the client code either permits access, sends them to a login screen, or shows a relevant dialog — though, as I hinted, that dialog might not appear at all. That's a topic for the next installment.
Key Takeaways
A quick recap of the ground covered. First, we explored a technique for overcoming missing browser objects in third-party libraries during the build phase — no messing with library internals required. Second, we looked at a practical method to selectively bypass server-side rendering, particularly when dealing with authorization constraints. The next article will pick up exactly where this one leaves off.
