When interviewing into Angular related jobs, one question I used to get asked, is “Demonstrate a simple data reloading when user clicks on a button” (demonstrated on the gif).
This question is tricky, because instead of jumping into the code, as a more experienced developer, you should as back “Do you want the solution to be imperative or declarative? 🤔”
This question will throw them off, increasing your perceived value in their eyes. Let’s describe what is the difference between these two approaches.
All the source code can be found on a stack blitz example.
API Layer
About the API, I am using a very nice little Quote API - https://github.com/lukePeavey/quotable
@Injectable({
providedIn: 'root',
})
export class QuotesApiService {
private url = 'https://api.quotable.io';
private http = inject(HttpClient);
getRandomQuote(): Observable<QuoteData> {
return this.http
.get<QuoteData[]>(`${this.url}/quotes/random`)
.pipe(map((response) => response[0]));
}
}
Imperative Approach
Imperative approach is the most common one, and probably the one that you follow. The idea is that when you click on then button you trigger the (click)
event, call an onDataReload()
method, issue API call, subscribe on the stream and pass the received result to some internal property / signal. The example is demonstrated below.
@Component({
selector: 'app-button-click-normal',
template: `
<div class="wrapper">
<div class="wrapper-text">
Loaded Quote: {{ loadedQuoteSignal()?.content }}
</div>
<div class="wrapper-action">
<button type="button" (click)='onQuoteLoad()'>Load Quote</button>
</div>
</div>
`,
standalone: true,
styles: [],
})
export class ButtonClickNormalComponent implements OnInit {
private quotesApiService = inject(QuotesApiService);
loadedQuoteSignal = signal<null | QuoteData>(null);
ngOnInit() {
this.onQuoteLoad();
}
onQuoteLoad() {
this.quotesApiService.getRandomQuote().subscribe((res) => {
this.loadedQuoteSignal.set(res);
});
}
}
This is called “imperative approach”, because you “provide step-by-step” guide what to do.
- Using the
ngOnInit
you preload a data - On every button click you call the
onQuoteLoad
. - Create manually a new Http call
- Manually subscribe and pass data into the signal
Now, by all means, this approach is not bad. It very readable, easily debuggable, and every developer will know what is going on. This is the approach I personally tend use…. However on interviews you have to flex, you have to show who is the boss here and that you can demonstrate different solutions for the same problem and you are able to describe the benefits of each of them.
Declarative Approach
In a nutshell, declarative programming consists of ”instructing a program on what needs to be done, instead of telling it how to do it” ← Yes, I copied this from google.
Let’s take a look how could we change the same example into a declarative approach.
@Component({
selector: 'app-button-click-reactive',
template: `
<div class="wrapper">
<div class="wrapper-text">
Loaded Quote: {{ (loadedQuote$ | async)?.content }}
</div>
<div class="wrapper-action">
<button #loadQuoteButton type="button">Load Quote</button>
</div>
</div>
`,
standalone: true,
imports: [CommonModule],
})
export class ButtonClickReactiveComponent {
private quotesApiService = inject(QuotesApiService);
@ViewChild('loadQuoteButton', { static: true, read: ElementRef })
loadQuoteButton!: ElementRef<HTMLButtonElement>;
loadedQuote$ = defer(() =>
fromEvent(this.loadQuoteButton.nativeElement, 'click').pipe(
startWith(null),
switchMap(() => this.quotesApiService.getRandomQuote())
)
).pipe(share());
}
Okey, this is a little bit weird, if you have never tried creating event listeners. To briefly describe what’s going on:
- You create viewChild reference on your button, setting the
{static: true}
, to resolve reference only once, before the first change detection happens. This can be a separate post, so for more info read here. - You want to create an Observable which will emit every time the user clicks on the button so you use
fromEvent(this.loadQuoteButton.nativeElement, 'click')
- You apply
startWith(null)
so that your stream has some initial value and you go straight to loading data withswitchMap(() => this.quotesApiService.getRandomQuote())
- What is
defer
? - It creates an Observable only when the Observer subscribes. Not using defer, the application will throw an errorCannot read properties of undefined (reading 'nativeElement')
. In my understanding, it creates an Observable when the view is initialized, meaning it already passed theAfterViewInit
lifecycle, but I can be wrong .Click here to read more about it. - Finally applying
pipe(share())
to allow multiple subscriptions without triggering data reload for each subscription, only if user clicks
Summary
Being a developer involves being able to come up with multiple solutions on the same problem. This is a simple problem which can involve two different solutions. I personally am the fan of imperative approach. The declarative one looks “more fancy”, but I haven’t seen it being used on any production project. Still, it is good to know something like that exists.
Give it a like if you found it helpful and connect with me on
dev.to | LinkedIn| Personal Website | Github
Top comments (2)
The imperative approach suffers from a race condition when the "Load Quote" button is clicked (again) before the previous api call is received. That is why I prefer the declarative approach but your example could be greatly simplified.
Since the Observable is being re-assigned as a result of the click output event in the html template, it automatically plays nice with change detection.
you are right, you approach will work. It just depends if you want to use the async pipe in the template or signals. I agree with the declarative approach, for me it wins because it is easier to read.