Tutorial

Manage State in Angular with NGXS

Published on January 27, 2019
author

Seth Gwartney

Manage State in Angular with NGXS

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.

CQRS-style State Management

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.

  1. There is only one single source of truth, managed by a store.
  2. Stored state is immutable; it cannot changed directly.
  3. Changes to state are made by reducers, functions which take as arguments the current state and an action to take on the state, and returns a completely new state object after the action has done its operation.

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.

Angular State Management with NGXS

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.

State

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) { }
}

Actions

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.

Selecting

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[]>;
}

Memoized Selectors

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[]>;
}

Store Service Selects

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)

Conclusion

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.

Further Reading

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar
Seth Gwartney

author

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.

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


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!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Become a contributor for community

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

DigitalOcean Documentation

Full documentation for every DigitalOcean product.

Resources for startups and SMBs

The Wave has everything you need to know about building a business, from raising funding to marketing your product.

Get our newsletter

Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

New accounts only. By submitting your email you agree to our Privacy Policy

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.