Akita (Concept): A State Management Pattern Based on Observables.

Akita: A State Management Pattern Based on Observables (AKA, Why Your State Management Shouldn’t Make You Cry)

Alright, class, settle down! Today, we’re diving into the majestic world of Akita, a state management pattern that, unlike some of its competitors (you know who I’m talking about ๐Ÿ‘€), aims to make your life as a developer easier, not harder.

Think of Akita as your friendly neighborhood state butler. It handles all the messy bits โ€“ fetching data, updating it, and keeping everyone informed โ€“ so you can focus on building kick-ass user interfaces.

(โœจ Imagine a tiny, impeccably dressed Shiba Inu, bowing slightly. That’s Akita. โœจ)

Why Another State Management Solution? (The Sad Story of State Management Grief)

Before we gush over Akita, let’s acknowledge the elephant in the room: there are tons of state management solutions out there. Redux, MobX, NgRx, Vuex… the list goes on. So, why bother with another one?

Well, the honest truth is, many of these solutions can feelโ€ฆ well, complicated. Remember those times you spent hours debugging a simple state update because of some intricate reducer logic? Or wrestling with immutability rules that felt more like a cruel joke? ๐Ÿ˜ญ

Akita aims to address these pain points. It’s built on the following principles:

  • Simplicity: It’s designed to be easy to learn and use. You don’t need a PhD in functional programming to understand Akita’s core concepts.
  • Maintainability: Akita promotes a clear and organized state structure, making it easier to reason about and maintain your application’s state over time.
  • Flexibility: It’s not tied to any specific framework. While it’s commonly used with Angular, it can theoretically be adapted to other frameworks that support Observables.
  • Observable-Based: It leverages the power of Observables to provide a reactive and efficient way to manage state.

(๐Ÿค” Think of it this way: Akita is the Marie Kondo of state management. It helps you tidy up your state, keep only what sparks joy, and get rid of the clutter. KonMari method for your data! โœจ)

The Core Concepts: Building Blocks of State Zen

Akita’s architecture is based on three core concepts:

  1. Store: The single source of truth for your application’s state. It holds the data and provides methods for updating it. Think of it as your application’s brain. ๐Ÿง 
  2. Query: A way to select and transform data from the store. Queries are reactive and will automatically update whenever the store changes. Think of it as the eyes of your application, always watching the store and reporting what’s happening. ๐Ÿ‘€
  3. Service: A class that encapsulates the business logic for updating the store. Services are responsible for fetching data from the server, performing any necessary transformations, and then updating the store. Think of it as the arms and legs of your application, interacting with the outside world and bringing back information to the brain. ๐Ÿฆพ

Let’s break down each of these concepts in more detail:

1. The Store: The Repository of Truth (and Maybe Some Cookie Crumbs)

The store is where all the action begins. It holds the state of your application and provides methods for updating it.

  • Stores are Singular: You typically have one store per feature. For example, you might have a TodosStore for managing your to-do list, a UsersStore for managing user data, and so on.

  • Extending Store: In Akita, you create a store by extending the Store class.

    import { Store, StoreConfig } from '@datorama/akita';
    
    export interface TodosState {
      todos: Todo[];
      loading: boolean;
      error: any;
    }
    
    export function createInitialState(): TodosState {
      return {
        todos: [],
        loading: false,
        error: null
      };
    }
    
    @StoreConfig({ name: 'todos' }) // Important: Unique name for the store
    export class TodosStore extends Store<TodosState> {
      constructor() {
        super(createInitialState());
      }
    }
    
    export const todosStore = new TodosStore(); // Instantiate the store
    • @StoreConfig({ name: 'todos' }): This decorator is crucial. It tells Akita the name of your store. This name is used for debugging and for storing the state in local storage (if you choose to persist your state).
    • createInitialState(): This function defines the initial state of your store. It’s important to provide a default value for each property in your state.
    • super(createInitialState()): This calls the constructor of the Store class, passing in the initial state.
  • Updating the Store: Akita provides several methods for updating the store:

    • update(newState: Partial<State>): Merges the provided newState with the existing state. This is the most common way to update the store.

      todosStore.update({ loading: true }); // Set loading to true
    • update(id: ID, newState: Partial<State>): Updates a specific entity in the store (if you’re using an entities store, which we’ll cover later).

    • set(newState: State): Replaces the entire state with the provided newState. Use this with caution! It can be useful for resetting the store to its initial state.

    • setError(error: any): Sets the error property of the state. Useful for handling errors from API calls.

2. The Query: Your Window into the State (Without the Window Bugs)

Queries are the way you access data from the store. They’re essentially selectors that allow you to pick and choose the data you need, and they’re reactive, meaning they automatically update whenever the store changes.

  • Queries are Reactive: Queries return Observables, which emit a new value whenever the store changes. This allows you to easily update your UI in response to state changes.

  • Creating Queries: You create queries by extending the Query class.

    import { Query } from '@datorama/akita';
    import { TodosState, TodosStore } from './todos.store';
    import { Todo } from './todo.model';
    import { Observable } from 'rxjs';
    import { map } from 'rxjs/operators';
    
    export class TodosQuery extends Query<TodosState> {
      constructor(protected store: TodosStore) {
        super(store);
      }
    
      // Select all todos
      getTodos(): Observable<Todo[]> {
        return this.select(state => state.todos);
      }
    
      // Select only completed todos
      getCompletedTodos(): Observable<Todo[]> {
        return this.select(state => state.todos.filter(todo => todo.completed));
      }
    
      // Select the loading state
      isLoading$(): Observable<boolean> {
        return this.selectLoading(); // Akita provides helper methods like selectLoading()
      }
    }
    
    export const todosQuery = new TodosQuery(todosStore); // Instantiate the query
    • super(store): The query needs a reference to the store it’s querying.
    • this.select(state => ...): This is the core of the query. It takes a selector function that receives the state as input and returns the data you want.
    • this.selectLoading(): Akita provides helper methods for common state properties like loading, error, and entities.
  • Using Queries in Your Components: You can subscribe to queries in your components to get updates whenever the data changes.

    import { Component, OnInit } from '@angular/core';
    import { TodosQuery } from './todos.query';
    import { Todo } from './todo.model';
    import { Observable } from 'rxjs';
    
    @Component({
      selector: 'app-todos',
      templateUrl: './todos.component.html',
      styleUrls: ['./todos.component.css']
    })
    export class TodosComponent implements OnInit {
      todos$: Observable<Todo[]>;
    
      constructor(private todosQuery: TodosQuery) { }
    
      ngOnInit() {
        this.todos$ = this.todosQuery.getTodos();
      }
    }
    <ul>
      <li *ngFor="let todo of todos$ | async">{{ todo.title }}</li>
    </ul>
    • todos$ | async: The async pipe automatically subscribes to the Observable and unsubscribes when the component is destroyed, preventing memory leaks.

3. The Service: The Logic Handler (and Data Fetcher Extraordinaire)

Services are responsible for encapsulating the business logic for updating the store. They’re the bridge between your components and the outside world (e.g., your API).

  • Services Decouple Logic: Services keep your components clean and focused on presentation. All the data fetching, transformation, and state update logic lives in the service.

  • Services are Injectable: In Angular, services are typically injected into components using dependency injection.

  • Creating Services:

    import { Injectable } from '@angular/core';
    import { TodosStore } from './todos.store';
    import { Todo } from './todo.model';
    import { HttpClient } from '@angular/common/http';
    import { tap } from 'rxjs/operators';
    
    @Injectable({ providedIn: 'root' })
    export class TodosService {
      private apiUrl = 'https://your-api.com/todos';
    
      constructor(private todosStore: TodosStore, private http: HttpClient) { }
    
      // Fetch todos from the API
      getTodos() {
        this.todosStore.update({ loading: true }); // Set loading state
        return this.http.get<Todo[]>(this.apiUrl).pipe(
          tap(todos => {
            this.todosStore.update({ loading: false }); // Reset loading state
            this.todosStore.update({ todos }); // Update the store with the new todos
          }, error => {
            this.todosStore.update({ loading: false, error }); // Handle errors
          })
        );
      }
    
      // Add a new todo
      addTodo(todo: Todo) {
        return this.http.post<Todo>(this.apiUrl, todo).pipe(
          tap(newTodo => {
            const currentTodos = this.todosStore.getValue().todos;
            this.todosStore.update({ todos: [...currentTodos, newTodo] }); // Update the store
          })
        );
      }
    
      // Update a todo
      updateTodo(todo: Todo) {
        return this.http.put<Todo>(`${this.apiUrl}/${todo.id}`, todo).pipe(
          tap(updatedTodo => {
            const currentTodos = this.todosStore.getValue().todos;
            const updatedTodos = currentTodos.map(t => t.id === updatedTodo.id ? updatedTodo : t);
            this.todosStore.update({ todos: updatedTodos }); // Update the store
          })
        );
      }
    
      // Delete a todo
      deleteTodo(id: string) {
        return this.http.delete(`${this.apiUrl}/${id}`).pipe(
          tap(() => {
            const currentTodos = this.todosStore.getValue().todos;
            const updatedTodos = currentTodos.filter(todo => todo.id !== id);
            this.todosStore.update({ todos: updatedTodos }); // Update the store
          })
        );
      }
    }
    • @Injectable({ providedIn: 'root' }): This makes the service injectable throughout your application.
    • this.todosStore.update(...): The service uses the update() method to update the store with the fetched data or the results of any transformations.
    • tap(...): The tap operator allows you to perform side effects (like updating the store) without modifying the emitted value. This is a common pattern in Akita services.
    • Error Handling: The service should handle errors from API calls and update the store accordingly (e.g., by setting the error property).

Entities Stores: Managing Collections Like a Pro

Akita provides a specialized type of store called an Entities Store, which is designed for managing collections of entities (e.g., a list of users, a list of products, etc.). Entities Stores provide a number of helpful methods for adding, updating, and removing entities from the store.

  • EntityState and EntityStore: To use an Entities Store, you need to extend the EntityState interface and the EntityStore class.

    import { EntityState, EntityStore, StoreConfig } from '@datorama/akita';
    import { Todo } from './todo.model';
    
    export interface TodosState extends EntityState<Todo> {
      loading: boolean;
      error: any;
    }
    
    @StoreConfig({ name: 'todos' })
    export class TodosStore extends EntityStore<TodosState, Todo> {
      constructor() {
        super();
      }
    }
    
    export const todosStore = new TodosStore();
    • EntityState<Todo>: This tells Akita that the store will manage a collection of Todo entities.
    • EntityStore<TodosState, Todo>: The second type parameter to EntityStore specifies the type of the entities being managed.
  • Entities Store Methods: Entities Stores provide a number of helpful methods for managing entities:

    • add(entity: Entity | Entity[]): Adds one or more entities to the store.
    • update(id: ID, entity: Partial<Entity>): Updates an entity with the given ID.
    • remove(id: ID | ID[]): Removes one or more entities from the store.
    • set(entities: Entity[]): Replaces the entire collection of entities with the provided array.
    • upsert(id: ID, entity: Partial<Entity>): Updates an entity if it exists, or adds it if it doesn’t.
  • Queries for Entities Stores: Akita also provides specialized query methods for Entities Stores:

    • selectAll(): Returns an Observable of all entities in the store.
    • selectEntity(id: ID): Returns an Observable of the entity with the given ID.
    • selectMany(ids: ID[]): Returns an Observable of an array of entities with the given IDs.
    • selectLoading(), selectError(): Same as with regular Stores.

Putting It All Together: The Akita Workflow (A Symphony of State)

Let’s recap the typical Akita workflow:

  1. Define your state: Create an interface that defines the structure of your state (e.g., TodosState).
  2. Create a store: Extend the Store or EntityStore class and define the initial state.
  3. Create a query: Extend the Query class and define methods for selecting and transforming data from the store.
  4. Create a service: Create a service that encapsulates the business logic for updating the store (e.g., fetching data from the API, adding new entities, etc.).
  5. Use the query in your components: Subscribe to the query in your components to get updates whenever the data changes.
  6. Call the service methods in your components: Call the service methods in your components to trigger state updates.

(๐ŸŽถ Think of it as a musical performance: The Store is the orchestra, the Query is the conductor, and the Service is the composer. They all work together to create a beautiful symphony of state! ๐ŸŽถ)

Akita vs. The Competition: The State Management Hunger Games

So, how does Akita stack up against other state management solutions?

Feature Akita Redux NgRx
Learning Curve Relatively Easy Steep Very Steep
Boilerplate Low High Very High
Immutability Encouraged, but not enforced. Enforced (Requires Reducers) Enforced (Requires Reducers, Actions, Effects)
Observables Core Concept Requires Middleware (e.g., Redux-Observable) Core Concept
Entities Support Built-in Entity Stores Requires Custom Implementation Requires Custom Implementation
Debugging Built-in DevTools (e.g., Akita DevTools) Redux DevTools Redux DevTools
Framework Framework Agnostic, commonly with Angular Framework Agnostic Angular

In a nutshell:

  • Choose Akita if: You want a simple, maintainable, and flexible state management solution that’s easy to learn and use. Especially good for teams familiar with OOP principles.
  • Choose Redux if: You need a predictable and debuggable state management solution with a large ecosystem of middleware and tools. (Be prepared for boilerplate!)
  • Choose NgRx if: You’re building a complex Angular application and want a robust and scalable state management solution that integrates well with RxJS. (Be prepared for a steep learning curve and lots of boilerplate!)

Conclusion: Embrace the Akita Spirit (And Stop Crying Over State)

Akita is a powerful and flexible state management pattern that can help you build more maintainable and scalable applications. Its focus on simplicity, coupled with the power of Observables, makes it a compelling choice for developers of all skill levels.

So, embrace the Akita spirit! Say goodbye to state management headaches and hello to a world of clean, organized, and reactive applications. Now go forth and build amazing things! ๐Ÿš€

(๐ŸŽ‰ Class dismissed! Go forth and conquer the world of state management, armed with the knowledge of Akita! And remember, keep your state clean, your queries sharp, and your services well-defined. You’ve got this! ๐ŸŽ‰)

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *