4.0.3 • Published 2 months ago

@jacobtipp/bloc v4.0.3

Weekly downloads
-
License
MIT
Repository
-
Last release
2 months ago

@jacobtipp/bloc

Introduction

@jacobtipp/bloc is an npm package that helps implement the BLoC (Business Logic only Components) pattern.

Installation

rxjs is a peerDependency

npm install @jacobtipp/bloc rxjs

Cubit

A cubit is a class that extends BlocBase and is a primitive type of Bloc that can be extended to manage any type of state.

Cubit Architecture

This is similar to stores like Zustand. Your Cubit encapsulates state and you have methods of the Cubit that mutate the state by invoking this.emit.

Creating a Cubit

import { Cubit } from "@jacobtipp/bloc"

export class CounterCubit extends Cubit<number> {
  constructor() {
    super(0); 
  }

  increment(): void {
    this.emit(this.state + 1);
  }

  decrement(): void {
    this.emit(this.state - 1);
  }
}

Using a Cubit

function main() {
  const counterCubit = new CounterCubit();

  console.log(counterCubit.state); // 0

  counterCubit.increment();

  console.log(counterCubit.state); // 1

  // cubit is closed and disposed, it will no longer emit new state and all observers will be unsubscribed
  cubit.close();
}

Observing a Cubit

When a Cubit emits a new state, a Change occurs. We can observe all changes for a given Cubit by overriding onChange.

import { Cubit } from "@jacobtipp/bloc"

export class CounterCubit extends Cubit<number> {
  constructor() {
    super(0); 
  }

  protected override onChange(change: Change<number>): void {
    console.log(change); // { current: 0, nextState: 1 }
  }
  
  increment(): void {
    this.emit(this.state + 1);
  }

  decrement(): void {
    this.emit(this.state - 1);
  }
}

Bloc

A Bloc is a more advanced class which relies on events to trigger state changes rather than functions. Bloc also extends BlocBase which means it has a similar public API as Cubit. However, rather than calling a function on a Bloc and directly emitting a new state, Blocs receive events and convert the incoming events into outgoing states.

Bloc Architecture

Creating a Bloc

import { Bloc } from "@jacobtipp/bloc"

/// The events which `CounterBloc` will react to.
abstract class CounterEvent {
  protected _!: void
}

class CounterIncrement extends CounterEvent {}

class CounterDecrement extends CounterEvent {}

export class CounterBloc extends Bloc<CounterEvent, number> {
  constructor() {
    super(0);

    this.on(CounterIncrement, (event, emit) => emit(this.state + 1));

    this.on(CounterDecrement, (event, emit) => emit(this.state - 1));
  }
}

Using a Bloc

async function main() {
  const delay = (n) => new Promise((resolve) => setTimeout(resolve, n));

  const counterBloc = new CounterBloc();

  console.log(counterBloc.state); // 0

  counterBloc.add(new CounterIncrement());

  // wait for next tick in event loop
  await delay(0);

  console.log(counterBloc.state); // 1

  counterBloc.close(); // close the bloc when no longer needed
}

Observing a Bloc

Similar to Cubits, a Bloc can be observed by overriding onChange, however, because Bloc is event-driven, we are also able to capture information about what triggered the state change.

We can do this by overriding onTransition.

The change from one state to another is called a Transition. A Transition consists of the current state, the event, and the next state.

import { Bloc } from "@jacobtipp/bloc"

export class CounterBloc extends Bloc<CounterEvent, number> {
  constructor() {
    super(0);

    this.on(CounterIncrement, (event, emit) => emit(this.state + 1));

    this.on(CounterDecrement, (event, emit) => emit(this.state - 1));
  }

  protected override onTransition(transition: Transition<CounterEvent, number>): void {
    console.log(transition); // { currentState: 0, event: CounterIncrement, nextState: 1 }
  }
}

Error Handling

Both Bloc and Cubit have an addError and onError method. We can indicate that an error has occurred by calling addError from anywhere inside our Bloc. We can then react to all errors by overriding onError just as with Cubit.

import { Bloc } from "@jacobtipp/bloc"

export function assertIsError(error: unknown): asserts error is Error {
  if (!(error instanceof Error)) {
    throw error;
  }
}

export class CounterBloc extends Bloc<CounterEvent, number> {
  constructor(private readonly counterRepo: CounterRepository) {
    super(0);

    this.on(CounterIncrement, this.onIncrement);
  }

  private onIncrement(event: CounterIncrement, emit: Emitter<number>) {
    try {
      await this.counterRepo.increment()
      this.emit(this.state + 1)
    } catch (e: unknown) {
      assertIsError(e);
      addError(e);
    }
  }

  protected override onError(error: Error): void {
    console.log(error: Error) // Error object with stack trace
  }
}

BlocObserver

BlocObserver is a class that you can extend to listen to all Blocs/Cubits that exist at runtime.

This allows you to trace multiple lifecycle events for all Blocs/Cubits.

// app-bloc-observer.tsx

export class AppBlocObserver extends BlocObserver {
  override onCreate(bloc: BlocBase<any>): void {
    console.log(`onCreate: ${bloc.name}`);
  }

  override onEvent(bloc: Bloc<any, any>, event: any): void {
    console.log(`onEvent: ${bloc.name}`)
  }

  override onError(bloc: BlocBase<any>, error: any): void {
    console.log(`onError: ${bloc.name}`, error);
  }

  override onChange(bloc: BlocBase<any>, change: Change<any>): void {
    console.log(`onChange: ${bloc.name}`, change);
  }

  override onTransition(
    bloc: Bloc<any, any>,
    transition: Transition<any, any>
  ): void {
    console.log(`onTransition: ${bloc.name}`, transition)
  }

  override onClose(bloc: BlocBase<any>): void {
    console.log(`onClose: ${bloc.name}`);
  }
}
// main.tsx
// composition root of your application

BlocObserver.observer = new AppBlocObserver();

Event Transformations

bloc has an event sink that allows us to control and transform the incoming flow of events.

For example, if we were building a real-time search, we would probably want to debounce the requests to the backend in order to avoid getting rate-limited as well as to cut down on cost/load on the backend.

With bloc we can provide a custom EventTransformer to change the way incoming events are processed by the bloc.

Event Transformation is only available for Bloc and not Cubit.

import { Bloc, EventMapper, EventTransformer } from '@jacobtipp/bloc';
import { Observable, debounceTime, mergeMap } from 'rxjs';

// A debounce event transformer
export const debounce =
  <Event>(duration: number): EventTransformer<Event> =>
  (events$: Observable<Event>, mapper: EventMapper<Event>): Observable<Event> =>
    events$.pipe(debounceTime(duration), mergeMap(mapper));

export class CounterBloc extends Bloc<CounterEvent, number> {
  constructor() {
    super(0);

    // apply the debounce event transformer to the event handler
    this.on(CounterIncrement, (event, emit) => emit(this.state + 1), debounce(200));
  }
}

⚠️ Please refer to the bloc-concurrency package, which has commonly used Event Transformers.

Observable

Both Bloc and Cubit extend BlocBase which exposes a state$ property, which is an rxjs observable stream. This can be used to directly subscribe to the state stream.

function main() {
  const counterCubit = new CounterCubit();

  counterCubit.state$.subscribe((state) => {
    console.log(state); 
  });

  counterCubit.increment();

  cubit.close();
}
4.0.3

2 months ago

4.0.3-next.1

2 months ago

4.0.2

2 months ago

4.0.1

3 months ago

4.0.0

3 months ago

4.0.0-next.4

3 months ago

4.0.0-next.3

3 months ago

4.0.0-next.2

3 months ago

4.0.0-next.1

3 months ago

3.1.0

4 months ago

3.0.3

5 months ago

2.0.3

7 months ago

2.0.2

7 months ago

2.0.4

7 months ago

2.0.1

8 months ago

2.0.0

8 months ago

3.0.2

5 months ago

3.0.1

6 months ago

3.0.0

6 months ago

1.1.1

8 months ago

1.1.0

8 months ago

2.0.5-dev-bloc.1

6 months ago

3.0.0-dev-bloc.1

6 months ago

2.1.0-dev-bloc.1

6 months ago

2.1.0-dev-bloc.2

6 months ago

2.0.0-dev.1

8 months ago

2.0.0-dev.3

8 months ago

2.0.0-dev.2

8 months ago

2.0.0-dev.5

8 months ago

2.0.0-dev.4

8 months ago

2.0.0-dev.7

8 months ago

2.0.0-dev.6

8 months ago

1.0.2

1 year ago

1.0.1

1 year ago

1.0.0

1 year ago

1.0.4

1 year ago

1.0.3

1 year ago

1.0.0-beta.4

1 year ago

1.0.0-beta.5

1 year ago

1.0.0-beta.10

1 year ago

1.0.0-beta.6

1 year ago

1.0.0-beta.7

1 year ago

1.0.0-beta.8

1 year ago

1.0.0-beta.9

1 year ago

1.0.0-beta.2

1 year ago

1.0.0-beta.3

1 year ago

1.0.0-beta.1

1 year ago