DEV Community

Cover image for Simplify Angular Local State Management with Component Store
Smruti Ranjan Rana
Smruti Ranjan Rana

Posted on • Edited on • Originally published at smrutiranjanrana.hashnode.dev

Simplify Angular Local State Management with Component Store

Hey there! Just so you know, handling states in a front-end app is super important for making sure everything works smoothly and gives users a fantastic experience. The state is all about the up-to-date data and settings of the user interface, and it can change based on how users interact with the app or even due to outside events. By managing the state like a pro, you'll be able to steer the app's behaviour and make sure it responds just right to user input.

When it comes to managing the state of a front-end application, there are a bunch of cool techniques you can try out. Some popular options include using state management libraries like Redux or NGRX, working with component-based state management in frameworks like React or Angular and applying the Flux architecture. Plus, you can also take advantage of global state objects, local storage or session storage, and create your custom state management solutions to keep your app's behaviour and user experience in check.

Let's have some fun exploring state management together in Angular. πŸ˜„

Prerequisites

  • A basic understanding of Angular

  • A basic grasp of RxJs

Now, let's jump in and get started! πŸš€

Let's start

Service + Subject = ❀️

Service and Subject in Angular are well-liked for state management as they're simple and easy to put into action. Services help share data and methods across components, while Subjects, a kind of RxJS observable, let you use reactive programming to manage asynchronous data streams. Together, they offer a hassle-free way to handle the state without extra libraries or tricky setups.

That being said, this method does have some downsides, like struggling with scalability in big applications, making it tough to track state changes, and not having built-in tools for dealing with side effects. These challenges might encourage developers to look for stronger state management solutions that have the right architecture for managing states and ensuring a more predictable data flow.

Libraries from NGRX

The @ngrx/store and @ngrx/component-store libraries provide powerful state management solutions for Angular applications.

The Store library is perfect for managing the global state, making your data flow predictable and handling side effects like a breeze. Meanwhile, the ComponentStore library is all about managing local component states, giving you a lightweight and adaptable way to manage states within individual components. Plus, both libraries use the Redux pattern and RxJS observables to build efficient, reactive apps with a rock-solid architecture.

In this article, we'll mainly focus on how to use @ngrx/component-store for managing local states like pros. 😊

Component Store

This nifty little library is incredibly lightweight, with just one file containing all 524 lines of code (as of now) needed to manage your states. But don't let its size fool you - it's a powerful, high-performance, and thoroughly tested library that's sure to impress!

It's not that different from the Service with Subject approach, but the ComponentStore does the heavy lifting for you to manage observable streams and also provides a well-defined architecture to follow.

It is more structured than Service with Subject but it's not quite complex like the full @ngrx/store approach. It's a nice middle ground.

Just like any state management library, in this library also, we have methods to read, write and side-effects.

Read states

select()

The select() method is like a friendly helper that creates and gives you an observable representation of a piece of your local component state. It makes it super easy for you to access and respond to specific parts of the state.

readonly #cart$ = this.select((state) => state.cart);
Enter fullscreen mode Exit fullscreen mode

What's more? You can use it to combine multiple selectors too! By doing so, you can craft more intricate and derived state slices from what's already there. This comes in handy when you need to access various parts of the state and mix them meaningfully.

 readonly #cart$ = this.select((state) => state.cart);
 readonly #cartValue$ = this.select(this.#cart$,
    (cart) => cart.reduce((acc, item) => acc + item.product.price * item.quantity, 0)
 );
Enter fullscreen mode Exit fullscreen mode

get()

The get() method is super useful when you need to access the state imperatively, especially within effects. It's designed to be used only within the ComponentStore, so it's kept private.

Although, frequent imperative reads might lead to some not-so-great architectural choices. Just remember, a selector is a go-to way to read the state reactively via Observable.

Write states

updater()

It's kind of like a reducer in Redux! The updater() method helps you create state update functions that change the component's local state in a friendly manner. It needs two things:

  1. A callback function that takes the current state and a payload and then returns the updated state.

  2. A default value for the payload.

The cool thing about updater() is that it makes sure state updates are done in an immutable and reactive way.

 readonly addProduct = this.updater((state: CartState, product: ProductInterface) => {
    return {
        ...state,
        cart: [...state.cart, { id: uuid4(), product, quantity: 1 }],
    };
 });
Enter fullscreen mode Exit fullscreen mode

setState()

Just like updater(), setState() is a handy way to update the states in the ComponentStore. Usually, we use setState() for lazy initialization. To use it, all you need to do is provide the state object, and the whole state will be reset to the value you give. Keep in mind that, unlike updater(), setState() doesn't return any function - it simply returns void.

addProduct(product: ProductInterface) {
    this.setState((state) => ({
        ...state,
        cart: [...state.cart, { id: uuid4(), product, quantity: 1 }],
    }));
}
Enter fullscreen mode Exit fullscreen mode

patchState()

patchState() is quite similar to setState(), but with a little twist! Instead of providing the entire state object, you can update states partially.

addProduct(product: ProductInterface) {
    this.patchState((state) => ({
        cart: [...state.cart, { id: uuid4(), product, quantity: 1 }],
    }));
}
Enter fullscreen mode Exit fullscreen mode

Side effects

effect()

The effect() method is a super useful tool for handling side effects and both synchronous and asynchronous operations. It lets you do tasks that are set off by specific actions, without directly changing the state. This nifty method helps you manage things like API calls in a reactive and tidy manner, and it's pretty great at giving you control over race operators too!

Just a quick note: You can send either observable or regular values as parameters when using the effect() method. No worries, it'll always automatically convert the parameters to observable for you. Pretty cool, right?

readonly #fetchProducts = this.effect(
    (params$: Observable<ProductListParams>) =>
        params$.pipe(
            tap(() => {
                this.setLoading(true);
            }),
            switchMap(({ size, page, query }) => {
                return this.#productService.getProducts(size, page, query).pipe(
                tapResponse(
                    (response) => {
                        this.setProducts(response.products);
                        this.setError(null);
                },
                (error: HttpErrorResponse) => {
                    this.setError(error.error?.message ?? error.message);
                },
                () => {
                    this.setLoading(false);
                }
            ));
        })
     )
);
Enter fullscreen mode Exit fullscreen mode

Map operators

Let's have a chat about some awesome higher-order map operators that you can use in effects. They're super handy!

mergeMap()

This operator is pretty cool. It subscribes right away and doesn't cancel or discard anything. It's usually handy when you're deleting items.

Example: Imagine you have a table filled with data, and each row has a button to delete it. When users click on multiple delete buttons for different rows, they'll call the same method but with different IDs to delete. In this case, mergeMap() is super helpful because it calls all the APIs simultaneously without waiting for any of them. So, every time a user hits the delete button, the API is called in parallel.

concatMap()

This one subscribes after the last one finishes, and it's typically used when updating or creating items.

Example: Imagine you have a form, and when you click the submit button, it calls the API to save the value. Now, let's say the user clicks the submit button multiple times. No worries! concatMap() will call the APIs one after another, only after the last one finishes.

exhaustMap()

This operator helps you out by discarding extra calls until the last one is done. It's perfect for situations where you don't need to change any parameters.

Example: Picture this - you have a refresh button to update your table data. When you click the refresh button, the query parameters stay the same. In this case, you can use exhaustMap() to make sure it calls the API just once and ignores any extra clicks while the first one is still pending. No stress!

switchMap()

Now, this little buddy cancels the previous one if it hasn't finished yet. It's usually a great choice for queries with specific parameters.

Example: Imagine you've got a paginator and you're clicking the next button a bunch of times. This means the API will be called for every page, but it's uncertain which page's API will finish first. You might end up with previous data in the table because the earlier API was completed last. No worries, though! You can use switchMap() to help out. It cancels any previous APIs that are still pending and waits for the latest one's result. That way, you can be sure you'll have the right data to display in the table.

concatMap() is the safest operator!

concatMap() is a pretty safe operator since it makes sure everything stays in order by subscribing to the next observable only after the previous one finishes. This helps avoid any mix-ups and ensures that the results are processed just as they should be.

If you're ever unsure about which operator to pick, concatMap() is a solid choice. Just keep in mind that it might cause some performance hiccups in certain situations, like when you have a bunch of API calls waiting in line.

How to start?

Ready to begin? Simply run the following command to add the ComponentStore to your Angular app:

ng add @ngrx/component-store@latest
Enter fullscreen mode Exit fullscreen mode

Initialization

Now, let's talk about initialization!

Using the constructor method

When you initialize through the constructor, the state becomes instantly accessible to the ComponentStore users.

export interface MoviesState {
    movies: Movie[];
}

@Injectable()
export class MoviesStore extends ComponentStore<MoviesState> {
    constructor() {
        super({ movies: [] });
    }
}
Enter fullscreen mode Exit fullscreen mode

Lazy initialization

Sometimes, you might not want selectors to show any state until there's some useful data in the ComponentStore. No worries, you can initialize the state lazily! Just call setState() and pass the full state to it. You can use the same method to reset the state too.

Just a heads-up, make sure to initialize before updating the state, or else you might run into an error.

export interface MoviesState {
    movies: Movie[];
}

@Injectable()
export class MoviesStore extends ComponentStore<MoviesState> {
    constructor() {
        this.init(); // you can call it from your component as well
    }

    init() {
        this.setState({ movies: [] })
    }
}
Enter fullscreen mode Exit fullscreen mode

Component Store use cases

Let's explore some fantastic use cases for leveraging ComponentStore!

Singleton state

You can use the ComponentStore as a global store too! When you're creating the component file, simply include provideIn: 'root' within injectable(). This will set up the ComponentStore file as a singleton.

Common component state

The ComponentStore is super versatile and can be used in shared components too. To manage the states of a shared component, just attach a store file to the component itself. This way, whenever you use the component in various places, it'll create a unique instance of the ComponentStore for that shared component.

Local component state

Just so you know, you can easily handle the local states of your component by simply linking a ComponentStore file with the component itself. This creates an isolated data flow, making everything neat!

Component "branch" state

If you've got a parent component with several child components and you'd like to share some common states among them, no worries! Just create a ComponentStore file with those shared states and link it up with all the child components. This way, they can all access the same info, making your life a whole lot easier!

Conclusion

In a nutshell, we can make ComponentStore our go-to buddy for handling local states. If we're faced with more complex states or need to juggle multiple global states, Store is here to help!

A cool plan could be: for local state management, let's team up with ComponentStore and for global state management, we'll count on Store.

Just keep in mind that the perfect approach might be different for each project, so stay flexible and have fun!

Choose Component Store

Hopefully, when you will think about managing states in your application, you will choose ComponentStore over Service with Subject. It provides a simpler and more reactive way to handle state management, leading to better performance and a smoother user experience. Give it a try and enjoy the benefits!

Feel free to ask if you have any more questions or give feedback. Happy coding!

Demo app - https://github.com/devsmranjan/slipkart

Learn more - https://ngrx.io/guide/component-store

Top comments (0)