-
Notifications
You must be signed in to change notification settings - Fork 16
Open
Description
Hey, I am the person that responded to your reddit post.
TL;DR
- This is a whole lot, but I like explaining things. Feel free to ask followups, but I am hoping to frontload most things, as well as provide key terms you can look up in your own time.
- This issue focuses on one particular aspect of how to structure your code, how state could be handled in a more reactive and declarative way, but it is a rich topic.
- I provide three Stackblitz examples for alternatives to the
app.component.ts
and how the files are fetched. The benefits of this refactor are spread out across the projects that I made which recreated the basic flow. Notes are in code comments in the main component file and a service defined above it, as well as some HTML unordered lists on each Stackblitz that you can read as you test out the demo.- First example project sets the state of the original files in a fairly normal way to see in projects, but values which then depend on it are much more reactive.
- Second example project is fully declarative by doing some extra RXJS stuff - likely overkill, but your scenario here is also a bit more complex than many components out there which just load something unconditionally on page load. I talk about how easy that is in a couple spots across these examples.
- Third example using Angular signals from Angular 16+, with a bit of RXJS still for handling the HTTP - arguably the easiest, but could be mixed with some things of the other two approaches.
How I structured my examples of the code, and the overall benefits
I noticed that there is one method getFiles()
that handles resetting and then setting assorted state, as well as the respective HTTP parameters. I think that with a few tweaks, that method and all of its responsibilities could be simplified or even removed.
Before I summarize the three approaches, some overall similarities
- The HTTP param setting logic was moved into the same service that has the HTTP call. I mocked this via an rxjs
delay(1000)
aka one second, but you could just put the param logic in thegetFormatedFiles
of yourDataService
- Anything like the HTTP params and other HTTP stuff like headers tend to be put into the same methods in a service with the HTTP method itself, rather than done in a component.
- The
isLoading
logic was moved into the service as well. This may be more of a personal preference, but in most projects I have been on or followed when it came to handling HTTP in services or stores that call them, the loading status tends to be put in services. I think it would be fine in the component as well though.- If you did include the loading in your service, I would do the loading of true before the call and then in the end use the RXJS operator
finalize
to set the loading back to false. RXJS has plenty of specific operators, butfinalize
is really slick for scenarios like this since it handles when the observable terminates from both being completed or from an error. Pretty common to see it in the wild, but mostly for this scenario.
- If you did include the loading in your service, I would do the loading of true before the call and then in the end use the RXJS operator
- The pattern of the loading state being declared twice, once in a private settable state and then once as a readonly public variable that is a mirror of the settable one, is called the "Service with a Subject" or "Service with a Signal" approach.
- This is a fairly common way to handle state in Angular projects that do not use a state library. You can find plenty on that if you look around, but the nice part is that nothing outside of the service can mutate it yet you can still read it elsewhere. Good examples you see of this approach that also handle HTTP probably will also handle the loading state and do the
finalize
convention.
- This is a fairly common way to handle state in Angular projects that do not use a state library. You can find plenty on that if you look around, but the nice part is that nothing outside of the service can mutate it yet you can still read it elsewhere. Good examples you see of this approach that also handle HTTP probably will also handle the loading state and do the
- Many variables such as
srcPaths
anddstPaths
were made to be "reactive" and "declarative". TheoriginalFiles
is also both of those things after the first example. You can find plenty of things if you search around with those terms, but the big benefits are- One class variable declaration handles the whole definition of its state. It is not re-assigned elsewhere.
- Variables like
srcPaths
anddstPaths
can react to other changes and also have their whole state handled. This is done either by using an RXJS stream to map the values in relation tooriginalFiles$
, or in example number three with the relatively new Angular signals API. - Example number three is arguably the easiest and most future friendly in Angular, as once you handle setting some asynchronous action's signal, then you can reactively declare other values as just
computed
values from some signal it depends on, rather than the terminology of RXJS with its pipes and some other fancy operators.
Overall summary of the three approaches
- Manual subscribe event such as a button click.
- Probably the most normal thing you would see, but still more reactive than plenty of projects.
- You would just want to be sure to unsubscribe after the call, otherwise there may be memory leaks. I commented out an operator called
takeUntilDestroyed
that is introduced in Angular 16 that is quite nice for scenarios like this. - The setting of the value through a function which subscribes is referred to as imperative, and the values that depend on the value which is then set are considered reactive/declarative. If what you take away from all these fancy suggestions is that you can make some values fully declarative and react to other values even if those reacted-to values are manually (imperatively) set, then you would be excelling beyond a ton of code I have seen in the wild. In particular, the next point:
- Lets say you had a component that just loads some data when it is initialized, rather than on a button press. You could merely declare
files$ = this.serviceName.getFiles()
, and once you need that in the template just usefiles$ | async
or in the componentfiles$.pipe(...)
to declare other values orfiles$.subscribe(...)
. No need to declare files in a function, or assign it in a constructor orngOnInit
.
- Subject event such as a button click (fully reactive with RXJS)
- This one makes the
originalFiles
itself fully declarative, but does add some more RXJS understanding overhead. - A
Subject
that stands in for the button click event happening. Then when that occurs, theswitchMap
operator "switches" into the HTTP observable and "maps" its value to the files observable. - I don't tend to use subjects as often, but I wanted to show off how you could be very reactive.
- A much more common scenario that you could apply some of this advice to much more easily: if you were in a scenario where you didn't need a button click or other event to load the files or something else, aka you just want to set a value on load, then it could simply be
files$ = this.serviceName.getFiles()
, or in example 3,$files = toSignal(this.serviceName.getFiles()
. People normally do something like this in a method that is in a constructor or ngOnInit, but they are getting none of the reactive/declarative benefits
- This one makes the
- toSignal event such as a button click
- This uses Angular signals which were introduced in Angular 16, and the new control flow (
@if
instead of*ngIf
and whatnot) introduced in v17. The current version of Angular is 18.2, and you could probably upgrade to it at your project's scale if you think it is worth it. - Signals are in many, many scenarios much easier to deal with than observables, since signals excel in synchronous values, and RXJS excels in async things like HTTP or events like button clicks.
- Angular has functions like
toSignal
to allow events like thegetFilesEvent$
and the inner mapping of the HTTP call to be ultimately be handled in a synchronous manner. The src/dst paths don't need to be piped, don't need the async pipe, and otherwise are treated synchronously as something reacting to the changing of the original files. Also, theshareReplay(1)
is not needed since once the inner observable computes, the signal value for the files is purely synchronous and anything that depends on it isn't retriggering an observable stream. - You are quite likely to be able to do something like
$files = toSignal(this.serviceName.getFiles()
in most other scenarios. Or if you want to be like scenario one where a button click manually subscribes, do that scenario but rather than.next
you.set
a signal. - RXJS and Signals work well together, but signals may eventually make RXJS in Angular optional. In my opinion, use them to their strong spots and handle them with the
toSignal
ortoObservable
as needed. - The other values just used for two way binding do not technically need to be signals at the moment, but at some future point, values that change in a component should be made into signals so that Angular can optimally handle change detection.
- Angular has functions like
- The new control flow replaces
*ngIf/*ngSwitch/*ngFor
with@if/@switch/@for
, and has other benefits such as- Notice how in the first two stackblitz examples, I had to use an
*<div ngIf="...; else loading">...</div> <ng-template #loading>...</ng-template>
to be able to do a mere else. With the new control flow, there is built in@else
and@else if (condition)
. There is similar wins in the other two types as well. - The new for loop renders big arrays of things more optimally
- Notice how in the first two stackblitz examples, I had to use an
- This uses Angular signals which were introduced in Angular 16, and the new control flow (
AIxHunter
Metadata
Metadata
Assignees
Labels
No labels