DEV Community

Cover image for How to Make Angular Universal Behave Like NextJS
Jonathan Gamble
Jonathan Gamble

Posted on • Updated on

How to Make Angular Universal Behave Like NextJS

Can we really get all the benefits of NextJS in Angular Universal?. It is arguably hard to learn, especially when compared to newbie frameworks like SvelteKit, but it is still incredibly powerful. Let's see what's possible.

1. GetServerSideProps

First thing is first, let's handle the state transfer from the server to the browser. Create a new service like so:

ng g s shared/state
Enter fullscreen mode Exit fullscreen mode

state.service.ts

import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { makeStateKey, TransferState } from '@angular/platform-browser';

@Injectable({
  providedIn: 'root'
})
export class StateService {

  isBrowser: Boolean;
  isServer: Boolean;

  constructor(
    @Inject(PLATFORM_ID) platformId: Object,
    private transferState: TransferState
  ) {
    this.isBrowser = isPlatformBrowser(platformId);
    this.isServer = isPlatformServer(platformId);
  }

  saveState<T>(key: string, data: any): void {
    this.transferState.set<T>(makeStateKey(key), data);
  }

  getState<T>(key: string, defaultValue: any = []): T {
    const state = this.transferState.get<T>(makeStateKey(key), defaultValue);
    this.deleteState(key);
    return state;
  }

  hasState<T>(key: string): boolean {
    return this.transferState.hasKey<T>(makeStateKey(key));
  }

  deleteState<T>(key: string): void {
    this.transferState.remove<T>(makeStateKey(key));
  }

  async loadState<T>(promise: Promise<T>, key: string): Promise<T> {
    if (this.isBrowser && this.hasState<T>(key)) {
      return this.getState<T>(key);
    }
    const data = await promise;
    if (this.isServer) {
      this.saveState<T>(key, data);
    }
    return data;
  }
}
Enter fullscreen mode Exit fullscreen mode

This is boilerplate code, you should NEVER have to change it. You also have access to the functions to check for server or browser.

Next, generate a new component with a resolver. The resolver is what you use to load items before a component, but only from the router.

ng g r my-resolver
Enter fullscreen mode Exit fullscreen mode

Make sure to use that resolver in your router:

my-router.module.ts

const routes: Routes = [
  { path: '', component: MyComponent, resolve: { props: MyResolver } }
];
Enter fullscreen mode Exit fullscreen mode

Import the state transfer service, create a getServerSideProps function using the loadState function. This will automatically pass the information from the server to the browser. The loadState function also uses the url as the key here, but you could use anything. This function is reusable in many contexts.

my.resolver.ts

...
@Injectable({
  providedIn: 'root'
})
export class UsernameResolver implements Resolve<any> {

  constructor(
    private ss: StateService,
    private router: Router,
    ...
  ) { }

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Promise<any> {
    return this.ss.loadState(
      this.getServerSideProps({ urlQuery: route.params }),
      state.url
    );
  }

  async getServerSideProps({ urlQuery }: { urlQuery: any }) {

    const { slug } = urlQuery;
    ...

    return { user, posts };
}
Enter fullscreen mode Exit fullscreen mode

Import the data into your component:

my.component.ts

...
export class MyComponent {

  posts: any;
  user: any;

  constructor(private route: ActivatedRoute) {
    const { posts, user } = this.route.snapshot.data['props'];
    this.posts = posts;
    this.user = user;
  }
}
Enter fullscreen mode Exit fullscreen mode

2. GetStaticProps / GetStaticPaths

So there is no direct translation here, but you could write your own. For caching, you could use an in-memory cache or a file system cache.

However, what you really want is a cache-control header. What GetStaticPaths really does is fetch the names of the paths, and cache them once. Then, when you run your router live, it uses that cached data to decide which other paths should get cached etc. It is simple and incredibly complicated at the same time.

I personally find it more valuable to just cache all pages the first time they load (particularly in a blog), and have them un-cache when the data gets changed. This is easy in NextJS, but would require custom programming here. I have not written this yet, but I will save the specifics of this for another post.

3. One File per Component

I personally do not like JSX, but functional components have a huge advantage over classes: they are tree-shakable. That being said, we can combine all of our styles, ts, and html into one file. This is especially good for small components. Modern VS Code also handles the inline html as expected with validation, this used to not be the case.

Change your component generation:

ng g c test --inline-style=true --inline-template=true
Enter fullscreen mode Exit fullscreen mode

Or permanently:

angular.json

  "projects": {
    "some-project-name": {
      "projectType": "application",
      "schematics": {
        "@schematics/angular:component": {
          "inlineTemplate": true,
          "inlineStyle": true,
        }
Enter fullscreen mode Exit fullscreen mode

4. No More Modules

Angular now has standalone components, which allow you to skip modules use all together:

ng g c test
Enter fullscreen mode Exit fullscreen mode

Add the standalone: true to the component, and use the imports to import modules etc. Okay, so for now we can't completely get rid of modules since things like angular material still use them. I suspect this will change within two years.

test.component.ts

@Component({
  standalone: true,
  selector: 'app-test',
  imports: [ImageComponent],
  template: `
    ... <br><div class="something"...
  `,
})

Enter fullscreen mode Exit fullscreen mode

5. Add API Endpoints

Add an endpoint to your server.ts file:

import { handler } from 'handler';
...

server.get('/api/**', handler);
Enter fullscreen mode Exit fullscreen mode

I wrote a whole post on this here.

So not everything is perfect, but we are just ultimately using JavaScript under-the-hood. Svelte, Vue, or SolidJS is very quick (but not Qwik), but Angular and React are, in fact, pretty comparable these days. NextJS just build something better over React.

Let's push Angular Universal to its limit.

J

Top comments (0)