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
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;
}
}
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
Make sure to use that resolver in your router:
my-router.module.ts
const routes: Routes = [
{ path: '', component: MyComponent, resolve: { props: MyResolver } }
];
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 };
}
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;
}
}
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
Or permanently:
angular.json
"projects": {
"some-project-name": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"inlineTemplate": true,
"inlineStyle": true,
}
4. No More Modules
Angular now has standalone components, which allow you to skip modules use all together:
ng g c test
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"...
`,
})
5. Add API Endpoints
Add an endpoint to your server.ts file:
import { handler } from 'handler';
...
server.get('/api/**', handler);
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)