Rewriting Angular 10 Directly with Version 18
Upgrade from Angular 10 to 18
I donโt upgrade, I rewrite actually. ๐ฎโ๐จ
I recently realized it had been ages since I last played with Angular. Feeling a bit rusty, I decided to use my free time for a try - upgrading my old project from Angular 10 straight to 18. Looking at that massive official upgrade checklist, I was like... nah, let's just rewrite the whole thing, haha.
https://angular.dev/update-guide?v=10.2-18.0&l=1
Firebase Got a Makeover Too
First up, Firebase integration got super seamless:
ng add @angular/fire
This command's now like a thoughtful nanny, asking what features you want during installation and automatically registering everything in app.config.ts
.
And those pesky AngularXXX
prefixes are gone, leaving us with clean XXX
APIs.
Saying Goodbye to app.module.ts
The Old Way
Remember when we used to have that one big file, app.module.ts
, where we'd throw in everything and the kitchen sink? It was like the Grand Central Station of our app. We'd import all our modules there, declare our components, and set up our bootstrap. Then main.ts
would kick off the whole show.
It went something like this: main.ts โ app.module.ts โ app.component.ts
The New Hotness
Now, Angular 17+ has flipped the script. We've got this new player called app.config.ts
. Think of it as the app's command center, focusing on the big picture stuff and major service setups. It's not about listing every little detail anymore - it's all about managing the important, app-wide configurations.
The flow's gotten a lot simpler too: main.ts โ app.component.ts
Seems like vue?
const app = createApp(App).use(something)
And app.config.ts
? It's doing its own thing on the side, handling those crucial app-wide setups without getting in the way of your components.
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { provideFirestore, getFirestore } from '@angular/fire/firestore';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
provideFirestore(() => getFirestore())
]
};
Introducing Standalone Components
The new standalone component design is pretty neat. Here's the deal:
Components are now self-sufficient units. They bring along their own dependencies.
This design creates clearer boundaries between different parts of your app.
In the old days, you could dump all your imports into the NgModule, and even with lazy loading, you'd end up dragging the whole NgModule baggage along for the ride. It was like packing your entire wardrobe for a weekend trip.
But with the new standalone design, lazy loading got a lot smarter. Now, it only grabs the components it actually needs. Need something? Just import it right there in the component. No more relying on that one overstuffed suitcase (NgModule) for everything.
This change makes our apps more modular, easier to understand, and way more efficient when it comes to loading. It's like we've gone from a tangled web of dependencies to a neat, organized collection of independent modules.
@Component({
selector: 'app-child',
standalone: true,
imports: [FirestoreModule, CommonModule, FormsModule, MaterialModule],
templateUrl: './child.component.html',
styleUrls: ['./child.component.scss'],
animations: [listAnimation],
})
The Old vs. The New: A Quick Comparison
Feature | old NgModule imports | new app.config.ts |
Router | RouterModule.forRoot(routes) | provideRouter(routes) |
HTTP | HttpClientModule | provideHttpClient() |
Forms | FormsModule | in components |
Animation | BrowserAnimationsModule | provideAnimationsAsync() |
Dependency Injection: The Invisible Butler
Services in Angular now have this cool superpower. They can show up in your components without needing a formal import statement. It's like they have an all-access pass to your app.
Here's a quick example:
This service can now be used anywhere in your app without explicitly importing it. It's like having a butler who appears exactly when you need them, without you having to call.
And get this - you can choose how widely available you want your services to be:
root
: Like a singleton, available app-wide
any
: New instance each time (transient)
platform
: Shared across multiple apps (rare, but cool)
// remote-data.service.ts
@Injectable({
providedIn: 'root',
})
export class RemoteDataService {
private chunkCollection: CollectionReference<any>;
constructor(private firestore: Firestore) {
this.chunkCollection = collection(this.firestore, 'chunks');
}
For my C# buddies out there, here's how Angular's dependency injection scopes compare:
(Relying on My Past Experience, but Unsure if It Can Be Analogized in Different Situations)
https://v17.angular.io/api/core/Injectable#providedIn
Feature | C# Equivalent | Description |
<empty> | Scoped | manually register to your providers |
any (deprecated) | Transient | New instance every time |
OtherModule (deprecated) | Scoped | Shared the same instance |
root | Singleton | One instance for the whole app |
platform | - | Shared across multiple apps |
The Dependency Chain: How It All Connects
Scenario 1: Everything's Registered Properly
Imagine this: your component relies on a service, and that service needs Firestore.
If you've set things up right in app.config.ts
, it's smooth sailing. Your component can use everything without breaking a sweat.
Here's what it looks like in app.config.ts
:
export const appConfig: ApplicationConfig = {
providers: [provideFirestore(() => getFirestore())]
But heads up! If you forget to register stuff in app.config.ts
, things will go south real quick.
Scenario 2: Going Ruthless - Direct Firestore Action
Now, what if your component wants to chat with Firestore directly? No middleman service, no relying on app.config.ts
?
No problem! You can register services right in your component's imports
.
Here, we're bringing FirestoreModule into the imports
array. It's like telling Angular, "Hey, I need Firestore for this component, and I'm handling it myself." Then, in the constructor, we're actually injecting Firestore. It's DIY dependency injection at its finest!
This setup gives you more control and keeps your component self-contained. It's perfect when you need a specific service but don't want to mess up your global configuration.
@Component({
selector: 'app-child',
standalone: true,
imports: [FirestoreModule, CommonModule, FormsModule, MaterialModule],
templateUrl: './child.component.html',
styleUrls: ['./child.component.sass'],
animations: [listAnimation],
})
export class TechComponent implements OnInit {
constructor(private firestore: Firestore) {}
// ...
}
Providers vs Imports
- Providers are like the planners, telling Angular how to set things up.
- Imports are the actual guests showing up to the party.