Reader Stacks

Angular: Sharing Global Configuration and Building Reusable Components

A shared, injectable configuration object solves the global-variable problem the Angular way — and @Input/@Output are what turns a one-off component into a genuinely reusable one.

Angular doesn't have a "global variable" in the traditional sense, and reaching for one on the window object defeats the framework's own dependency injection system — a shared, injectable configuration service is the idiomatic way to make a value accessible application-wide, and it pairs naturally with the pattern for building genuinely reusable components.

A shared configuration service, instead of a global variable

@Injectable({ providedIn: 'root' })
export class AppConfigService {
    readonly apiUrl = 'https://api.example.com';
    readonly appName = 'My App';
    readonly maxUploadSizeMb = 10;
}
constructor(private config: AppConfigService) {}

ngOnInit() {
    console.log(this.config.apiUrl);
}

This achieves the same practical goal as a global variable — one shared value accessible anywhere — but through Angular's own dependency injection, which keeps it mockable in tests and visible as an explicit dependency in any component's constructor, unlike a value silently read off the global window object.

Environment-specific configuration

// environment.ts
export const environment = {
    production: false,
    apiUrl: 'http://localhost:8000/api',
};

// environment.prod.ts
export const environment = {
    production: true,
    apiUrl: 'https://api.example.com',
};
import { environment } from '../environments/environment';

console.log(environment.apiUrl);

Angular's build system automatically swaps in the correct environment file based on the build configuration (ng build --configuration production) — this is the standard place for genuinely environment-dependent values like API URLs, distinct from the AppConfigService pattern above, which suits values that don't change between environments.

Building a reusable component with @Input

@Component({
    selector: 'app-badge',
    template: `{{ text }}`
})
export class BadgeComponent {
    @Input() text: string = '';
    @Input() variant: 'success' | 'warning' | 'danger' = 'success';
}

@Input() properties are what make a component configurable from the outside — without them, a component's content and appearance would be hardcoded, defeating the point of reusing it in different contexts.

Emitting events from a reusable component with @Output

@Component({
    selector: 'app-confirm-button',
    template: ``
})
export class ConfirmButtonComponent {
    @Input() label: string = 'Confirm';
    @Output() confirmed = new EventEmitter();

    onConfirm() {
        this.confirmed.emit();
    }
}

@Output() with EventEmitter is how a reusable component communicates back up to whatever parent is using it — the parent decides what actually happens on confirmation (deleteItem() here), while the reusable component itself stays generic and unaware of that specific parent's logic.

Why this input/output pattern is the core of Angular component reusability

Together, @Input and @Output form a clear, explicit contract for a component — what it needs from its parent, and what it reports back — which is what actually makes a component genuinely reusable across many different parts of an application, rather than being written for one specific use case and copy-pasted with edits for the next.