[go: up one dir, main page]

DEV Community

Cover image for Easy state management in Angular
Akshay Mahajan
Akshay Mahajan

Posted on

Easy state management in Angular

Angular provides useful feature of services where we encapsulates all our Business Logic (BL) [back-end integration] inside the services. BL does includes persistence of state/data that would probably be meant for other components too. With increased components hierarchy, we tend to increase services that are associated with them which leads to application getting bloated and data communication between services and counter components gets messy.

Spaghetti Code

To fix this problem, we need opinionated state management and vast majority of solutions are already available in OSS market like NgRx, Ngxs, Akita, Elf, RxAngular etc. But this solution comes with a cost which is their associated learning curve and boilerplate code just to have its initial setup hooked into our application.

I'm Lost

To reduce this pain and get your system ready up (bare metal) and setup-ed in less time frame, I have created a dead simple state management solution in just less than 50 lines of code.

Yes, you hear it right, its just less than 50 lines of code. 😉

I'm Super happy now

I'm not gonna say that this is a full fledged state management solution that advanced libraries does. This is a bare metal need of state management which can suffice a need of many developers in their day to day task. For obvious reason when your task and need is more complex, one should consider using an opinionated state management libraries as stated above since they are tested well within the community and are scale-able enough.

So the basic fundamental of state management is to cache recurring data which is to be passed along a lot of component hierarchy. Input/Props drilling is one the issue where state management methodology like flux comes to resort. A central global store that will act as hydration of data to our components and probably act as single source to truth for many entities in your application.

So certain check list needs to be considered when implementing state management that is refer below.

✅ Central store for most of the entities (single source of truth).
✅ The store should be reactive (pushing instead of polling. Polling can be additional feature too).
✅ Select a certain slice of cached data.
✅ Update/Destroy the cached entity.
✅ No mutation for cached entity outside of reducer.

The state management solution that I'm gonna present is CRUD based. And this is gonna suffice 70-80% of use cases.

The syntax for function wrapper is gonna remind you of slice from Redux Toolkit.

Create a wrapper function

We are going to create a wrapper function that will help with the initial implementation of slice.

export function createSlice(opts) {
}
Enter fullscreen mode Exit fullscreen mode

Setting up Initial Data (🇨RUD)

This is the phase where we are going to create a slice with the initial state/data.

Typings for createSlice Options would look like:

export type CreateSliceOptions<T> = {
  initialValue: T;
};
Enter fullscreen mode Exit fullscreen mode

Using this type inside the function wrapper.

export function createSlice<T>(opts: CreateSliceOptions<T>) {
  let _value = opts.initalValue;
}
Enter fullscreen mode Exit fullscreen mode

Reading the value from inside the slice (C🇷UD)

We need to expose a function from inside the createSlice wrapper that will fetch us the current state inside the slice.

Typings for createSlice Instance would look like:

export type CreateSliceInstance<T> = {
  ...
 /**
  * Returns the current value of slice
  */
  getValue: () => T;
  ...
}
Enter fullscreen mode Exit fullscreen mode

Using this type inside the function wrapper.

  return {
    ...,
    getValue: () => _value;
  }
Enter fullscreen mode Exit fullscreen mode

Updating the data inside of slice (CR🇺D)

In order to update the slice, we will expose a method called update that will update the value inside the slice.

Let's add the update typing to the CreateSliceInstance.

export type CreateSliceInstance<T> = {
  ...
 /**
  * Callback to update the value inside the slice.
  */
  update: (state: Partial<T>) => void;
  ...
}
Enter fullscreen mode Exit fullscreen mode

Implementing the update method in the slice wrapper.

  return {
    ...,
    update: (state: Partial<T>) => {
      _value = state;
    }
  }
Enter fullscreen mode Exit fullscreen mode

How come this approach would be reactive? How would I subscribe to the changes I performed via update method?

In order to make our slice reactive, we need to re-adjust some implementation inside the createSlice wrapper, but although the typings will remain same.

NOTE: For now, the implementation only supports objects, and not primitive like string, number, boolean etc.

function createSlice<T>(opt: CreateSliceOptions<T>): CreateSliceType<T> {
  let _ob$ = new BehaviorSubject<T>(null);
  let _value = new Proxy(opt.initialValue ?? {}, {
    set: (target, property, value, receiver) => {
      const allow = Reflect.set(target, property, value, receiver);
      _ob$.next(target as T);
      return allow;
    },
  });
  return {
    valueChanges: _ob$.asObservable().pipe(debounceTime(100)),
    getValue: () => _ob$.getValue(),
    update: (state: Partial<T>) => {
      Object.keys(_value).forEach(key => {
        if (state.hasOwnProperty(key)) {
          _value[key] = state[key];
        }
      });
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

WOW, there are a lot of changes. Let's discuss them step-by-step:

  • We have created a BehaviorSubject that will emit the value inside it whenever we trigger next on it.
  • Instead of assigning initalValue directly to _value, we will create a new Proxy object, where we will override various handler methods on the target object. To read more about Proxy Pattern, refer this.
  • We will override the set method of the target object i.e. initialValue and will emit an new value, whenever a target is mutated.
  • For the update method, we will iterate over to the properties of the supplied state as param to update method and check if the property key in the state belongs to initialValue object and updating the _value[key]. The usage of hasOwnProperty will help us eradicate any miscellaneous (unknown) property from the slice's state.
  • We have use debounceTime in order to aggregate (iteration inside the update method) the changes in a certain time frame i.e. 100ms and will emit the target finally.

I hope this all makes sense to you all till now.

Deleting/Destroying the value inside the slice (CRU🇩)

When the slice is no longer in need, we can simply destroy the slice by calling the destroy on it.

Typing and implementation for destroy would be like:

   ...
   /**
   * Destroy the slice and closure data associated with it
   */
  destroy: () => void;
   ...
Enter fullscreen mode Exit fullscreen mode
return {
   ...,
   destroy: () => {
      _ob$.complete();
      // In case the target reference is used somewhere, we will clear it.
      _ob$.next(undefined);
      // Free up internal closure memory
      _value = undefined;
      _ob$ = undefined;
    },
   ...
}
Enter fullscreen mode Exit fullscreen mode

Resetting the slice's state (with intialValue)

There might be possibility where you might want to reset the state inside the slice.

Typings and implementation of reset would be like:

  ...
  /**
   * Reset the data with initial value
   */
  reset: () => void;
  ...
Enter fullscreen mode Exit fullscreen mode
return {
  ...,
  reset: () => {
      const {initialValue} = opt;
      Object.keys(initialValue).forEach(key => {
        _value[key] = initialValue[key];
      });
   },
  ...
}
Enter fullscreen mode Exit fullscreen mode

Complete Implementation

BONUS

If we see the implementation properly, the mutation can be possible via fetching target value from either getValue or valueChanges observable subscription. Although the mutation should not be happening outside the reducer (i.e. inside the slice context only).

We can fix this behaviour by wrapping the value inside the Object.freeze(target). Here is the revised implementation for getValue and valueChanges respectively.

return {
  ...,
  valueChanges: _ob$.asObservable().pipe(
      debounceTime(100),
      map(value => Object.freeze(value)),
  ),
  getValue: () => Object.freeze(_ob$.getValue()),
  ...
}
Enter fullscreen mode Exit fullscreen mode

Final thoughts

Thanks you for staying till here. You probably have learned something new today and that's a better version of you from yesterday's.
If you like this article, do give it a like or bookmark it for future reference. And if you feel there's need for some improvisation, do let me know in the comments. Would love to learn together.

Top comments (0)