What is state?
In interactive client-side applications, we may often find ourselves dealing with 'state'. Now, state can mean a different thing based on where you apply it, but the mental model I use, and apply towards state, relates to adjectives. What is an adjective? If we ask Google, we get:
An adjective is a word used to modify or describe a noun or pronoun.
So, if we take that model and apply it to our applications, we can say
state describes something about our application at a certain point in time.
But this isn't an article on state, what it is, or the different kinds of state you can have. However, we need to have a general understanding of what it is, so we know how we can deal with it. The act of dealing with state is called State Management, and in client-side applications, we often need to manage UI state, and have it persist to the server. UI state is one of the most common forms of state you'll manage as a front-end developer. We are talking about questions like, "is the menu open or closed? Do we have a checkmark next to our completed task?" etc ... UI state specifically describes how our UI components look and behave from that point in time.
Strategies to Manage UI State
There are plenty of amazing strategies to manage your UI state. Which strategy to use depends on your state, and the scope of that state. By scope, we mean will our entire application have access to this state (global)? Will the entire page (specific domain)? Or will just a singular component be concerned with the state, and completely hide it from the outside world (local). Today, I will show you a strategy I use to manage local UI state specifically with RxJS, and maintain state as a collection or list of things. Keep in mind that this is just one strategy! There are a ton of great options, and unfortunately, there is no one-size-fits-all solution to this complex problem.
The Story
Often times, when building UIs, we need to manage a list of things. We may need to add new things, read those things, update those things, and even delete those things. For example, we have a list of contacts here for our work directory. We see our contact's name and profile picture along with the ability to add and delete contacts or update the name of that existing contact. We need to reflect our changes to our user's screen, and we also need to ship these changes to the database so they are actually saved. This part is super important, and is called persisting the data
. If we didn't do this part, nothing really changes outside of our browser session!
A Solution
Typically, when working on problems, I'd like to start with a question. What do I have? Well, in this case, we will have a list of contacts. Let's take a look
@Injectable()
export class UserService {
constructor(private http: HttpClient) {}
contacts$ :Observable<Contact[]> = this.http.get<Contact[]>(url)
}
In this example above, we are setting a class property to the result of the get
function, which happens to be an Observable<Contact[]>
. This is getting us our data from the API that will be used in our template. This is how we consume it in the template:
@Component({
selector: 'app-component-overview',
template: `
<ng-container *ngIf="contacts$ | async as contacts">
<div *ngFor="let contact of contacts" >{{ contact.name }}<div/>
</ng-container>
`,
styleUrls: ['./component-overview.component.css']
})
export class ContactListComponent {
constructor(private userService: UserService){}
contacts$ = this.userService.contacts$;
}
Here, in our component, we capture a reference to the observable property, and set it to one we can more easily use in our template. Then, we consume it by subscribing to it in the template with the async
pipe. This is an important part because the async
pipe does some important things for us. First, it unwraps the value from the subscription. Second, it marks the component for change detection if that Observable emits again. Lastly, it takes care of all the tear down logic for us by unsubscribing when the component is to be destroyed.
This gets us a good bit of the way there, but we are still missing one critical piece. User action!
Action Streams
If all we are doing is taking data to show in our templates, subscribing with the async pipe and using the data in our templates is pretty standard. Some of the time, our data isnt read only, and our users will need to do something to the data. They may need to update, delete, or even add to the data. CRUD is an acronym for Create Read Update Delete and this captures some standard actions we take on data. Now, how can we perform these actions on the data we have? Lets enter in Action Streams. Action Streams are basically the act of capturing what the user is doing, and putting it into an Observable stream. Here's an example of an action stream to capture user clicks
,```ts import { fromEvent } from 'rxjs';
const clicks = fromEvent(document, 'click'); clicks.subscribe(x => console.log(x));
// Results in: // MouseEvent object logged to console every time a click // occurs on the document.
Here's a great visualization tool demonstrating how observables can fire. Checkout the mouse move example that demonstrates action streams [https://rxviz.com/](https://rxviz.com/)
The point is the user does something and we create an observable from that. Now, in the examples above, we are using Observable creation functions to achieve this. For our use case, we need to do something a bit different, because we want to control our streams, and the creation of the Observable. For this case, the Subject is a perfect tool.
## Subjects and Observables
From the docs...
> Every Subject is an Observable. Given a Subject, you can subscribe to it, providing an Observer, which will start receiving values normally.
> From the perspective of the Observer,
> it cannot tell whether the Observable execution is coming from a plain
> unicast Observable or a Subject.
> Every Subject is an Observer.
> It is an object with the methods next(v), error(e), and complete().
> To feed a new value to the Subject, just call next (theValue),
> and it will be multicasted to the Observers registered to listen
> to the Subject.
So what does this mean for us? Well, it means two things. First, we can use a Subject to emit user actions. Second, and this is the most important part, we can fire off those user emissions ourselves with the `next` method. This is what we need for our action streams.
```ts
submitCommand(action) {
this.contactsSubject.next(action);
}
Merge the Streams
So now we have our data stream (remember contacts$ :Observable<Contact[]> = this.http.get<Contact[]>(url)
), and our action stream. Our action stream will provide commands or instructions on what to do with our data. We need to merge those two streams together, do some computation and return our new value. Thanfully RxJS has us covered. Merging streams is done with the merge
creation function.
const resultingObservable$ = merge(dataStream$,actionStream$)
Pretty straightforward. Now every value flowing from each Observable will flow into that merged Observable.
Maintaining the State
Finally! We are almost there. We have values flowing into one place, and now we just need to let the backend know about our changes, and maintain some of that nice UI state. Anytime you're working in the pipe of an Observable, and you're going to be making an HTTP request, we know we are going to need a higher-order mapping operator. For the sake of time, here are some great talks going over all of the higher order mapping operators! Why Should You Care About RxJS Higher-order Mapping Operators? | Deborah Kurata and What GroupsBy in Vegas, Stays in Vegas - Mike Ryan & Sam Julien
Which operator you pick is important, and depends on the use case, but for our purposes, concatMap
is a good choice. concatMap
will map the value to an inner observable (our http req), subscribe to that observable, emit that value, and unsubscribe. Now, this is pretty much the case for all higher-order mapping operators, but 'concatMap' will, in the case of multiple observables, do them in the order in which they came, and will not move on to the next one until the one it is on completes. Here's our example:
const resultingObservable$ = merge(dataStream$,actionStream$).pipe(
concatMap(action => this.makeHttpRequest(action))
)
Now, after we've persisted the data back to the state, we need to maintain some state locally for our UI, and this is where we wrap it up!
Scan Operator
Last, but not least, we have our scan operator. Scan allows us to maintain some state of our accumulated values after performing some computation on them. Scan is alot like reduce. It takes in an accumulator function, and then returns each intermediate result in an Observable. Here's an an example.
const resultingObservable$ = merge(dataStream$,actionStream$).pipe(
concatMap(action => this.makeHttpRequest(action)),
scan((accumulatedVales, actionToPerform ) => this.performComputation(actionToPerform))
)
To tie this all together, lets recap our strategy.
Lastly here's a Stackblitz for a concrete example! https://stackblitz.com/edit/simple-entity-management