BLoC state management for react
Typescript implementation for react heavily inspired by flutter_react - https://bloclibrary.dev The BLoC Pattern (Business Logic Component) is a battle-tested design pattern for state management coming from Flutter and Dart. It tries to separate business logic from UI as much as possible while still being simple and flexible.
Full docs here (under construction): https://blac-docs.vercel.app
$ npm i blac
# or
$ yarn add blac
// CounterCubit.ts
export default class CounterCubit extends Cubit<number> {
increment = (): void => this.emit(this.state + 1)
}
// blacState.ts
const state = new Blac([new CounterCubit(0)]);
export const { useBloc } = state;
// CounterButton.tsx
const Counter: FC = (): ReactElement => {
const [value, { increment }] = useBloc(CounterCubit);
return <button onClick={() => increment()}>count is: {value}</button>;
}
The Blac
class handles the global state and manages all communication between individual BLoCs.
When initializing pass all the BLoCs for the global state in an array as first parameter.
const state = new Blac([new MyBloc(), new MyCubit()]);
You can add an observer to all state changes global and local
state.observer = new BlocObserver({
// onChange is called for all changes (Cubits and Blocs)
onChange: (bloc, event) => console.log({ bloc, event }),
// onTransition is called only when Blocs transition from one state to another,
// it is not called for Cubits
onTransition: (bloc, event) => console.log({ bloc, event }),
});
A Cubit is a simplified version Bloc
class. Create your custom Cubit by extending the Cubit
class, pass the initial
state to the super
constructor.
The Cubits' state is updated by calling the this.emit
method with the new state.
export default class CounterCubit extends Cubit<number> {
constructor() {
super(0);
}
increment = (): void => {
this.emit(this.state + 1);
};
}
In the react component you can then call the public methods like increment
in this example
const Counter: FC = (): ReactElement => {
const [value, { increment }] = useBloc(CounterCubit);
return <button onClick={() => increment()}>count is: {value}</button>;
}
Most of the time the Cubit
class will be the easiest way to manage a piece of state but for the more critical parts of
your application where there can be various reasons why the state changes to the same value, for example user
authentication. It might be nice to know if the user got logged out because an error occurred, the token expired or if
they just clicked on the logout button.
This is especially helpful when debugging some unexpected behavior.
In the BlocObserver
you can then use the onTransition
to see why the state changes, it will pass the previous state,
the event itself and the next state.
Use Enums or Classes as state to make it easier to debug.
export enum AuthEvent {
unknown = "unknown",
authenticated = "authenticated",
unauthenticated = "unauthenticated",
}
export default class AuthBloc extends Bloc<AuthEvent, boolean> {
constructor() {
super(false)
this.on(AuthEvent.unknown, (_, emit) => {
emit(false);
})
this.on(AuthEvent.unauthenticated, (_, emit) => {
emit(false);
})
this.on(AuthEvent.authenticated, (_, emit) => {
emit(true);
})
};
}
The following is the same example as above but with a class instead of an enum. One advantage to using classes is that you can pass properties in the class to use in the handler.
class AuthEvent {}
class AuthEventUnknown extends AuthEvent {}
class AuthEventUnAuthenticates extends AuthEvent {}
class AuthEventAuthenticated extends AuthEvent {}
export default class AuthBloc extends Bloc<AuthEvent, boolean> {
constructor() {
super(false)
this.on(AuthEventUnknown, (_, emit) => {
emit(false);
})
this.on(AuthEventUnAuthenticates, (_, emit) => {
emit(false);
})
this.on(AuthEventAuthenticated, (_, emit) => {
emit(true);
})
};
}
In your app you can then update the state by "adding" an event. Use the useBloc
hook to get access to the BLoC class
and add an event.
const Component: FC = (): ReactElement => {
const [state, bloc] = useBloc(AuthBloc);
return <>
{ state === true && <Button onClick = {()
=>
bloc.add(AuthEvent.unauthenticated)
}>
Logout < /Button>}
< />
}
The main way to use the BLoCs in your UI will be with the useBloc
hook, the first parameter is the class constructor
of the Bloc you want to use.
The second parameters are options to configure how that hook behaves.
The return value of useBloc
is an array of two items, the first is the state of your BLoC, the second is the class
which controls that part of the state.
Decide whether the returned state value should be updated or not. Will have no effect if subscribe
is false.
const [state] = useBloc(CounterCubit, {
// `state` is only updated if the count is even
shouldUpdate: (event) => event.nextState % 2 === 0,
});
If false
, the returned state will never be updated. Use this if you only need access to the BLoC class.
// we only need the `bloc` to call `bloc.increment` for example.
const [, bloc] = useBloc(CounterCubit, {
subscribe: false,
});
If you can't or don't want to use the useBloc
hook to access the bloc state you an alternatively opt for
the BlocBuilder
which is just a Higher Order Component that uses useBloc under the hood.
The blocClass
property takes a class constructor of a Bloc and provides an instance of that class in the builder
property.
The values passed into the callback for builder
are the same you would get from the useBloc
hook.
You can also pass the shouldUpdate
property to the BlocBuilder
which works the same as the option of the same name
for the useBloc
hook.
class Counter extends Component {
render() {
return <BlocBuilder<CounterCubit>
blocClass={CounterCubit}
builder={([value, { increment }]) => (
<div>
<Button onClick={() => increment()}>{value}</Button>
</div>
)}
/>;
}
}
The BlocBuilder can also be used to set the scope which can help performance when you don't want to rerender the whole component every time the state changes.