DEV Community

Cover image for Application State Management
Gleb Irovich
Gleb Irovich

Posted on

Application State Management

Welcome, everyone! In today's post, I would like to talk about the application state management. We will discuss what the state is and build a bare-bone state management solution with Typescript.

What is the state and why we need it?

Application state is a piece of information held together and can be accessed from different parts of your application. Data stored in the state is a snapshot of your program's dynamic properties in a given moment.
Why do we need it?

  • State helps to keep pieces of application in sync
  • Centralization makes an application more maintainable and the code more readable

Simple state

In an extremely simplified version, a state is just a JavaScript object. The state has some properties that different consumers can access. In the example below, our state keeps track of the count. stateConsumerA mutates the state by incrementing the count, while stateConsumerB logs state to the console.

interface State {
  count: number;
}

const state: State = {
  count: 0
};

function stateConsumerA() {
  state.count++;
}

function stateConsumerB() {
  console.log(state);
}

stateConsumerA();
stateConsumerB(); // log: {count: 1}
Enter fullscreen mode Exit fullscreen mode

What can we do better? One of the important requirements for the state is immutability. Immutability helps to prevent some undesired side-effects, which the mutation can cause. Moreover, immutability allows comparing different state snapshots to decide if an expensive operation should be performed.

Immutable state

Imagine your application being a public library and your state being a sacred book. As a library, you are willing to share this book's content, but you don't want it to be damaged. Therefore when someone requests that book, you send this person a copy.
Immutability in JavaScript is also achieved by creating a copy.

Consider an example below. We use an IIFE to encapsulate the application state in the closure and expose methods to read and update the state.

interface State {
  count: number;
}

interface StateStore {
  getState(): State;
  increment(): void;
}

const stateStore: StateStore = (function(): StateStore {
  const _state: State = {
    count: 0
  };

  return {
    getState: () => ({ ..._state }),
    increment: () => {
      _state.count++;
    }
  };
})();

function stateConsumerA() {
  stateStore.increment(); // original state count is incremented by one
  stateStore.getState().count = 100; // original state count is not mutated
}

function stateConsumerB() {
  console.log(stateStore.getState());
}

stateConsumerA();
stateConsumerB(); // log: {count: 1}
Enter fullscreen mode Exit fullscreen mode

You might notice that instead of returning actual state value, we create its shallow copy. Therefore, when stateConsumerA attempts to mutate the state object, it does not affect the output from the stateConsumerB.

One could alternatively implement it using ES6 classes, which will be our preferred approach for the rest of that post.

class Store {
  private state: State = {
    count: 0
  };

  public getState(): State {
    return { ...this.state };
  }

  public increment() {
    this.state.count++;
  }
}

const stateStore = new Store();
Enter fullscreen mode Exit fullscreen mode

Subscribing to state updates

Now, as you got an idea of what the state actually is, you might be wondering:
"OK, now I can update the state. But how do I know when the state was updated?".
The last missing piece is of cause subscribing to state updates. This is probably one reason why someone would bother about state management - to keep the application in sync.

There are a lot of brilliant state management solutions out there. But most of them have something in common - they rely on the Observer Pattern.
The concept is simple but though powerful. Subject keeps track of the state and its updates. Observers (in our case, state consumers) are attached to the subject and notified whenever the state changes.

type Observer = (state: State) => void;
Enter fullscreen mode Exit fullscreen mode

An observer, in our case, is just a function that takes State as an input and performs some operations with this state.
Let's create an Observer that logs if count is odd or even:

function observer(state: State) {
  const isEven = state.count % 2 === 0;
  console.log(`Number is ${isEven ? "even" : "odd"}`);
}
Enter fullscreen mode Exit fullscreen mode

Now we need to rework our Store class.

class Store {
  private state: State = {
    count: 0
  };

  private observers: Observer[] = [];

  public getState(): State {
    return { ...this.state };
  }

  public increment() {
    this.state.count++;
    this.notify(); // We need to notify observers whenever state changes
  }

  public subscribe(observer: Observer) {
    this.observers.push(observer);
  }

  private notify() {
    this.observers.forEach(observer => observer(this.state));
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's look at this example. Store, our Subject, contains information about the state and allows subscribing observers to the updates by adding them to the list and invoking with the latest state snapshot when it changes.
Here it is in action:

const stateStore = new Store();

stateStore.subscribe(observer);
stateStore.increment();
stateStore.increment();
stateStore.increment();
Enter fullscreen mode Exit fullscreen mode

Our code will produce the following output:

Number is odd
Number is even
Number is odd
Enter fullscreen mode Exit fullscreen mode

Although we haven't called our observer function, Subject does its job by notifying observers and calling them with the latest state snapshot.

Last but not least

The example discussed in this post is not exhaustive. In the real-world scenario, you should also take performance into account and unsubscribe, or detach observers, when necessary.

class Store {
  ...
  public unsubscribe(observer: Observer) {
    this.observers = this.observers.filter(item => item !== observer);
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

State management is an important topic. We deal with it regardless of the technology, and therefore I think it's important to know how it works under the hood.
Let me know if you find this topic interesting, and I will be happy to elaborate on this in the future.
If you liked my posts, please spread the word and follow me on Twitter πŸš€ and DEV.to for more exciting content about web development.

Top comments (1)

Collapse
 
spock123 profile image
Lars Rye Jeppesen

Great article, thanks.

I really love a state management library like @ngrx because provides the best of all worlds: strongly typed, observable reactive state management.

It feels really dirty to go back to state management without typing, or state management that is not based on Observables.