One common mistake in writing a front-end component is trying to fit everything in a single component. This can easily lead to unmaintainable code in the long run, especially for complex components.
In simple components, having many logics (e.g. API calls and binding to the form) written to the component itself sometimes works OK, especially if the aim is to reduce the file footprint. However, doing this to larger-sized components can make maintaining the code challenging. The last thing developers want to do is debug a component with 1000+ lines of code with intermingling logic.
Pros and cons of combining all logic into a single component:
Consider splitting your component's logic when:
constructor(private route: ActivatedRoute,private apiService: ApiService,) {}ngOnInit() {this.route.params.pipe(takeUntil(this.ngDestroy$),switchMap(params => this.apiService.load1(params.id)),).subscribe(response => {this.processApi(response);});}processApi(payload: ApiPayload1) {const calculatedData = this.calculate(payload);this.title = calculatedData.title;this.sumAmount = calculatedData.sum;}private calculate(...): ComponentData {// Calculate implementation}
❌ Figure: Massive amount of code intermingling from API calls to calculation to UI binding
Here are the steps to split the logic:
Observable and BehaviorSubject (or Signal, but this is still in developer preview) to bind value to UI elements. This will help us remove the need to imperatively notify the UI to re-render when the source value has changed.calculatedData$ = new BehaviorSubject<CalculatedData | null>(null);constructor(private route: ActivatedRoute,private apiService: ApiService,) {}ngOnInit() {this.route.params.pipe(takeUntil(this.ngDestroy$),switchMap(params => this.apiService.load1(params.id)),).subscribe(response => {this.calculatedData$.next(this.calculate(payload));});// UI Binding logicthis.calculatedData$.pipe(takeUntil(this.ngDestroy$),).subscribe(calculatedData => {this.title = calculatedData.title;this.sumAmount = calculatedData.sum;});}private calculate(...): ComponentData {// Calculate implementation}
✅ Figure: Use declarative code to bind UI value
// ComponentService ================constructor(private apiService: ApiService,) {}private _componentData$ = new BehaviourSubject<ComponentData|null>(null);public componentData$ = this._componentData$.asObservable();public initialiseComponentData(paramId: string): void {this.apiService.load1(params.id).subscribe(response => {this._componentData.next(this.calculate(payload));});}private calculate(...): ComponentData {// Calculate implementation}// Component ================constructor(private route: ActivatedRoute,private componentService: ComponentService,) {}ngOnInit() {// API Fetching logicthis.route.params.pipe(takeUntil(this.ngDestroy$),).subscribe(params => {this.componentService.initialiseComponentData(params.id);});// UI Binding logicthis.componentService.calculatedData$.pipe(takeUntil(this.ngDestroy$),).subscribe(calculatedData => {this.title = calculatedData.title;this.sumAmount = calculatedData.sum;});}
✅ Figure: UI logic is separated from Data Fetching and Data Processing logic
// ComponentService ================// ...Same implementation as above...// ParentComponent ================constructor(private route: ActivatedRoute,private componentService: ComponentService,) {}ngOnInit() {// API Fetching logicthis.route.params.pipe(takeUntil(this.ngDestroy$),).subscribe(params => {this.componentService.initialiseComponentData(params.id);});}// Component ================constructor(private componentService: ComponentService,) {}ngOnInit() {// UI Binding logicthis.componentService.calculatedData$.pipe(takeUntil(this.ngDestroy$),).subscribe(calculatedData => {this.title = calculatedData.title;this.sumAmount = calculatedData.sum;});}
✅ Figure: All logics (data fetching, data processing, and data display) are now separated