As front-end applications become increasingly complex, with the potential of multiple actors affecting the global state of the application in different ways, it becomes easy to lose control of what’s going on with your state. If you’ve never had to worry about managing global state in your JavaScript application, congratulations! You are one of the lucky ones. For the rest of us, there are a number of different libraries available.
If you aren’t familiar with CQRS (Command-Query Responsibility Segregation) or Redux, a popular library from the React world, here is a brief overview of some of the underlying principles for managing state.
In a previous article, we built a very basic document collaboration app using Angular and Socket.IO for real-time communication between a client and a server. This kind of application is a perfect use case for integrating a state management library like NGXS. When you have updates coming in at different times from different actors - in this case, the user can be making updates, and the server can be pushing updates - having a state container comes in handy. I’ll be using the app we built in that previous article as the basis for the examples in this article.
Start by installing the latest @ngxs/store
package from npm.
$ npm i @ngxs/store --save
You’ll be creating some new ES2015 classes that represent state and actions. It’s up to you how you want these file structured. For the sake of clarity, I’ll be writing everything like it’s one file, maybe called state.ts
. In reality, you’ll probably want to separate out these classes into their own file structure.
Now let’s create a couple of types to represent slices of our application state. These will be our state containers. You’ll also add some metadata about the state container.
import { State } from '@ngxs/store'
export interface DocumentStateModel {
id: string;
doc: string;
}
@State<DocumentStateModel>({
name: 'document', // required
defaults: { // optional: DocumentStateModel
id: '',
doc: 'Select an existing document or create a new one to get started'
}
})
export class DocumentState { }
@State<string[]>({
name: 'documentList',
defaults: ['']
})
export class DocumentListState { }
I could have put the state of the whole application in one @State
decorated class, with a property for the current document, and a property for the document list, but I’ve created two different state classes, DocumentState
and DocumentListState
to show you that you can have as many state containers in your store as you want.
State objects benefit from Angular’s dependency injection system, so if we wanted to inject a service into the state container we could:
export class DocumentListState {
constructor(private documentService: DocumentService) { }
}
Defining actions allows us to be very declarative in what our state should do in response to user events or things that have happened in the app.
Here is how we might define a simple action that adds a new document to the state:
export class AddDocument {
static readonly type = '[Document List] Add Document'; // required
}
Actions can also come with a payload. Let’s say the document we have open is being edited by someone else, and changes are being pushed down by our socket server. Our component may respond by dispatching an action that looks like this:
export class DocumentEditedFromServer {
static readonly type = '[Document] Edited From Server';
constructor(public docText: string) { }
}
Now back in our state classes, we need to define what should happen when our components dispatch these actions to the store.
export class DocumentState {
@Action(DocumentEditedFromServer)
editDocument(ctx: StateContext<DocumentStateModel>, action: EditDocument) {
const state = ctx.getState(); // always returns the freshest slice of state
ctx.setState({
...state,
doc: action.docText
}); // the spread operator is shorthand for Object.assign({}, state, { doc: docText });
}
}
We never modify the state directly; instead, we make a copy of the state, change whatever properties of the copy we need to, and set the whole state object to the our modified copy.
If you need to, your action can be performed asynchronously, by returning an Observable which sets the state in one of its pipable operators. An example might be an action which triggers an API call, and in the response, updating the state. You don’t need to subscribe to the Observable; the framework will do that for you, so you update the state within the pipe
chain, using a tap
operator, for example.
There are several ways to get data out of the store. First of all, we can define select properties in our components:
@Component({ /*...*/ })
export class DocumentListComponent {
// Stream of the entire Document List State
@Select(DocumentListState) documents$: Observable<string[]>;
// @Select doesn't need a parameter if the name of the
// property matches the name of the state you're selecting.
@Select() documentList$: Observable<string[]>;
// You can also use a function to get the slice of state you need.
@Select(state => state.documentList): Observable<string[]>;
}
If you have a specific function you need to use to select, which you may reuse, you can also memoize the static select function. In your state class:
@State<string[]>( /*...*/ )
export class DocumentListState {
@Selector()
static lastTenDocuments(state: string[]) {
return state.slice(-10);
}
}
Now in your component you can access the memoized selector.
export class DocumentListComponent {
@Select(DocumentListState.lastTenDocuments) recentDocuments$: Observable<string[]>;
}
You also have the option of injecting the store into your component like a service, and selecting from it directly.
export class DocumentComponent {
currentDocument$: Observable<DocumentStateModel>;
constructor(private store: Store) {
this.currentDocument$ = this.store.select(state => state.document);
}
}
And then you also have the option of selecting a static snapshot of state, in cases where you can’t use an Observable. Be aware that the state is only fresh at the time you select the snapshot. A good use case would be in an Interceptor class where you might need data at that moment, and subscribing wouldn’t make sense.
this.docId = this.store.selectSnapshot<string>(state => state.document.id)
We’ve barely scratched the surface of what NGXS can offer in terms of State Management. There are a host of other advanced patterns, features, tools, and plugins that NGXS and its community offers, which are beyond the scope of this article, but that we’ll hopefully be exploring in future articles.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.