bloc-react
Advanced tools
Comparing version 0.2.3 to 0.3.2
{ | ||
"name": "bloc-react", | ||
"version": "0.2.3", | ||
"version": "0.3.2", | ||
"license": "MIT", | ||
"main": "dist/bloc-react.js", | ||
"module": "dist/bloc-react.esm.js", | ||
"typings": "dist/bloc-react.d.ts", | ||
"main": "dist/blac.js", | ||
"module": "dist/blac.esm.js", | ||
"typings": "dist/blac.d.ts", | ||
"keywords": [ | ||
@@ -31,3 +31,3 @@ "react", | ||
"devDependencies": { | ||
"@babel/core": "^7.17.0", | ||
"@babel/core": "^7.17.2", | ||
"@babel/preset-env": "^7.16.11", | ||
@@ -51,4 +51,4 @@ "@babel/preset-react": "^7.16.7", | ||
"@types/react-router-dom": "^5.3.3", | ||
"@typescript-eslint/eslint-plugin": "^5.10.2", | ||
"@typescript-eslint/parser": "^5.10.2", | ||
"@typescript-eslint/eslint-plugin": "^5.11.0", | ||
"@typescript-eslint/parser": "^5.11.0", | ||
"@vitejs/plugin-react-refresh": "^1.3.6", | ||
@@ -58,4 +58,4 @@ "@wojtekmaj/enzyme-adapter-react-17": "^0.6.6", | ||
"enzyme": "^3.11.0", | ||
"esbuild": "^0.14.18", | ||
"eslint": "^8.8.0", | ||
"esbuild": "^0.14.21", | ||
"eslint": "^8.9.0", | ||
"eslint-config-prettier": "^8.3.0", | ||
@@ -69,3 +69,3 @@ "eslint-config-standard": "^16.0.3", | ||
"install": "^0.13.0", | ||
"jest": "27.5.0", | ||
"jest": "27.5.1", | ||
"jest-localstorage-mock": "^2.4.18", | ||
@@ -77,3 +77,3 @@ "jest-mock-console": "^1.2.3", | ||
"react-router-dom": "^6.2.1", | ||
"rollup": "^2.67.0", | ||
"rollup": "^2.67.2", | ||
"rollup-plugin-babel": "^4.4.0", | ||
@@ -88,3 +88,3 @@ "rollup-plugin-commonjs": "^10.1.0", | ||
"typescript": "^4.5.5", | ||
"vite": "^2.7.13" | ||
"vite": "^2.8.1" | ||
}, | ||
@@ -91,0 +91,0 @@ "jest": { |
237
readme.md
# BLoC React | ||
BLoC state management for react | ||
[![codecov](https://codecov.io/gh/jsnanigans/bloc-react/branch/main/graph/badge.svg?token=0FPH6ZMZD3)](https://codecov.io/gh/jsnanigans/bloc-react) | ||
[![liscence](https://img.shields.io/badge/license-MIT-purple.svg)](https://opensource.org/licenses/MIT) | ||
[![Known Vulnerabilities](https://snyk.io/test/github/jsnanigans/bloc-react/badge.svg)](https://snyk.io/test/github/jsnanigans/bloc-react) | ||
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=jsnanigans_bloc-react&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=jsnanigans_bloc-react) | ||
--- | ||
[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=jsnanigans_bloc-react&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=jsnanigans_bloc-react) | ||
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=jsnanigans_bloc-react&metric=security_rating)](https://sonarcloud.io/dashboard?id=jsnanigans_bloc-react) | ||
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=jsnanigans_bloc-react&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=jsnanigans_bloc-react) | ||
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=jsnanigans_bloc-react&metric=bugs)](https://sonarcloud.io/dashboard?id=jsnanigans_bloc-react) | ||
[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=jsnanigans_bloc-react&metric=code_smells)](https://sonarcloud.io/dashboard?id=jsnanigans_bloc-react) | ||
Typescript implementation for react heavily inspired by flutter_react - https://bloclibrary.dev | ||
The BLoC Pattern (**B**usiness **Lo**gic **C**omponent) 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. | ||
# Quickstart | ||
### 0. Install dependencies | ||
```shell | ||
$ npm i bloc-react | ||
# or | ||
$ yarn add bloc-react | ||
``` | ||
### 1. Create a new **Bloc/Cubit** | ||
```typescript | ||
// CounterCubit.ts | ||
export default class CounterCubit extends Cubit<number> { | ||
increment = (): void => this.emit(this.state + 1) | ||
} | ||
``` | ||
### 2. Create a new **BlocReact** instance and export `useBloc` from it | ||
```typescript | ||
// state.ts | ||
const state = new BlocReact([new CounterCubit(0)]); | ||
export const { useBloc } = state; | ||
``` | ||
### 3. Use the hook to access the state and class methods | ||
```typescript | ||
// CounterButton.tsx | ||
const Counter: FC = (): ReactElement => { | ||
const [value, { increment }] = useBloc(CounterCubit); | ||
return <button onClick={() => increment()}>count is: {value}</button>; | ||
} | ||
``` | ||
# Documentation | ||
## BlocReact | ||
The `BlocReact` 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. | ||
```typescript | ||
const state = new BlocReact([new MyBloc(), new MyCubit()]); | ||
``` | ||
You can add an observer to all state changes *global and local* | ||
```typescript | ||
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}), | ||
}); | ||
``` | ||
## Cubit | ||
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. | ||
```typescript | ||
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 | ||
```ts | ||
const Counter: FC = (): ReactElement => { | ||
const [value, { increment }] = useBloc(CounterCubit); | ||
return <button onClick={() => increment()}>count is: {value}</button>; | ||
} | ||
``` | ||
## Bloc | ||
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. | ||
```typescript | ||
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. | ||
```typescript | ||
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. | ||
```ts | ||
const Component: FC = (): ReactElement => { | ||
const [state, bloc] = useBloc(AuthBloc); | ||
return <> | ||
{state === true && <Button onClick={() => bloc.add(AuthEvent.unauthenticated)}>Logout</Button>} | ||
</> | ||
} | ||
``` | ||
## useBloc | ||
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](#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. | ||
### Options | ||
#### shouldUpdate | ||
Decide whether the returned state value should be updated or not. Will have no effect if `subscribe` is false. | ||
```ts | ||
const [state] = useBloc(CounterCubit, { | ||
// `state` is only updated if the count is even | ||
shouldUpdate: (event) => event.nextState % 2 === 0, | ||
}); | ||
``` | ||
#### subscribe | ||
If `false`, the returned state will never be updated. Use this if you only need access to the BLoC class. | ||
```ts | ||
// we only need the `bloc` to call `bloc.increment` for example. | ||
const [, bloc] = useBloc(CounterCubit, { | ||
subscribe: false, | ||
}); | ||
``` | ||
## BlocProvider | ||
Create a local state on demand with the `BlocProvider`. A context is created that then is available in the children of the BlocProvider. When the global and the local state both have the same Bloc, the local one is used. | ||
In the `bloc` property, pass a function that returns a new instance of the BLoC you want to provide. Or pass the BLoC directly. | ||
```ts | ||
const Counter: FC = (): ReactElement => { | ||
return <div> | ||
<BlocProvider<CounterCubit> | ||
bloc={() => new CounterCubit()} | ||
// bloc={new CounterCubit()} // can also be passed directly | ||
> | ||
<Counter /> | ||
</BlocProvider> | ||
</div>; | ||
} | ||
``` | ||
## BlocBuilder | ||
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. | ||
```ts | ||
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. | ||
# This project has been moved to <https://www.npmjs.com/package/blac> |
@@ -1,9 +0,9 @@ | ||
import { BlocReact } from "../../../src/lib"; | ||
import { BlacReact } from "../../../src/lib"; | ||
import BlocsCubit from "./BlocsCubit"; | ||
export const blocState = new BlocsCubit(); | ||
const state = new BlocReact( | ||
const state = new BlacReact( | ||
[blocState] | ||
); | ||
export const useBlocTools = state.useBloc; | ||
export const { useBloc } = state; |
import BlocBase from "./BlocBase"; | ||
import { BlocConsumer } from "./BlocConsumer"; | ||
import { BlacConsumer } from "./BlocConsumer"; | ||
@@ -18,3 +18,3 @@ describe("BlocBase", () => { | ||
try { | ||
bloc.consumer = new BlocConsumer([]); | ||
bloc.consumer = new BlacConsumer([]); | ||
} catch (e) { | ||
@@ -21,0 +21,0 @@ fail(e); |
@@ -1,2 +0,2 @@ | ||
import { BlocConsumer } from "./BlocConsumer"; | ||
import { BlacConsumer } from "./BlocConsumer"; | ||
import StreamAbstraction from "./StreamAbstraction"; | ||
@@ -11,3 +11,3 @@ import { BlocOptions, ChangeEvent } from "./types"; | ||
type ChangeMethod = <T>(change: ChangeEvent<T>, bloc: BlocBase<T>) => void | ||
type RegisterMethod = <T>(consumer: BlocConsumer, bloc: BlocBase<T>) => void | ||
type RegisterMethod = <T>(consumer: BlacConsumer, bloc: BlocBase<T>) => void | ||
type ValueChangeMethod = <T>(value: T, bloc: BlocBase<T>) => void; | ||
@@ -24,3 +24,3 @@ | ||
public valueChangeListeners: ValueChangeMethod[] = []; | ||
public consumer: BlocConsumer | null = null; | ||
public consumer: BlacConsumer | null = null; | ||
@@ -27,0 +27,0 @@ constructor(initialValue: T, blocOptions: BlocOptions = {}) { |
@@ -1,2 +0,2 @@ | ||
import { BlocConsumer } from "./BlocConsumer"; | ||
import { BlacConsumer } from "./BlocConsumer"; | ||
import BlocObserver from "./BlocObserver"; | ||
@@ -9,3 +9,3 @@ import { AuthEvent, ChangeListener, Test1, TestBloc, ValueChangeListener } from "../helpers/test.fixtures"; | ||
const testCubit = new Test1(); | ||
const testBlocConsumer = new BlocConsumer([testCubit]); | ||
const testBlocConsumer = new BlacConsumer([testCubit]); | ||
const onChange = jest.fn(); | ||
@@ -20,3 +20,3 @@ testBlocConsumer.observer = new BlocObserver({ onChange }); | ||
const testBloc = new TestBloc(); | ||
const testBlocConsumer = new BlocConsumer([testBloc]); | ||
const testBlocConsumer = new BlacConsumer([testBloc]); | ||
const onTransition = jest.fn(); | ||
@@ -38,3 +38,3 @@ testBlocConsumer.observer = new BlocObserver({ onTransition }); | ||
}); | ||
new BlocConsumer([testCubit]); | ||
new BlacConsumer([testCubit]); | ||
expect(register).toHaveBeenCalledTimes(1); | ||
@@ -45,3 +45,3 @@ }); | ||
const notify = jest.fn(); | ||
const consumer = new BlocConsumer([]); | ||
const consumer = new BlacConsumer([]); | ||
const bloc = new TestBloc(); | ||
@@ -56,3 +56,3 @@ bloc.addRegisterListener(notify) | ||
const bloc = new TestBloc(); | ||
const consumer = new BlocConsumer([]); | ||
const consumer = new BlacConsumer([]); | ||
consumer.addLocalBloc({ bloc, id: '909' }); | ||
@@ -69,3 +69,3 @@ consumer.observer.onTransition = notify; | ||
const bloc = new TestBloc(); | ||
const consumer = new BlocConsumer([]); | ||
const consumer = new BlacConsumer([]); | ||
consumer.addLocalBloc({ bloc, id: '909' }); | ||
@@ -85,3 +85,3 @@ consumer.observer.onTransition = notify; | ||
const listener = new ChangeListener(notify, Test1); | ||
const consumer = new BlocConsumer([global, listener]); | ||
const consumer = new BlacConsumer([global, listener]); | ||
consumer.addLocalBloc({ bloc: local, id: '909' }); | ||
@@ -101,3 +101,3 @@ expect(notify).toHaveBeenCalledTimes(0); | ||
const listener = new ChangeListener(notify, Test1, "local"); | ||
const consumer = new BlocConsumer([global, listener]); | ||
const consumer = new BlacConsumer([global, listener]); | ||
consumer.addLocalBloc({ bloc: local, id: '909' }); // should trigger listener "local" | ||
@@ -116,3 +116,3 @@ global.increment(); // should not trigger listener "local" | ||
const listener = new ChangeListener(notify, Test1, "global"); | ||
const consumer = new BlocConsumer([global, listener]); | ||
const consumer = new BlacConsumer([global, listener]); | ||
consumer.addLocalBloc({ bloc: local, id: '909' }); // should not trigger listener "global" | ||
@@ -132,3 +132,3 @@ expect(notify).toHaveBeenCalledTimes(0); | ||
const listener = new ChangeListener(notify, Test1, "all"); | ||
const consumer = new BlocConsumer([global, listener]); | ||
const consumer = new BlacConsumer([global, listener]); | ||
consumer.addLocalBloc({ bloc: local, id: '909' }); // should trigger listener "all" | ||
@@ -150,3 +150,3 @@ global.increment(); // should trigger listener "all" | ||
const listener = new ValueChangeListener(notify, Test1); | ||
const consumer = new BlocConsumer([global, listener]); | ||
const consumer = new BlacConsumer([global, listener]); | ||
consumer.addLocalBloc({ bloc: local, id: '909' }); | ||
@@ -167,3 +167,3 @@ expect(notify).toHaveBeenCalledTimes(0); | ||
const listener = new ValueChangeListener(notify, Test1, "local"); | ||
const consumer = new BlocConsumer([global, listener]); | ||
const consumer = new BlacConsumer([global, listener]); | ||
consumer.addLocalBloc({ bloc: local, id: '909' }); // should trigger listener "local" | ||
@@ -182,3 +182,3 @@ global.increment(); // should not trigger listener "local" | ||
const listener = new ValueChangeListener(notify, Test1, "global"); | ||
const consumer = new BlocConsumer([global, listener]); | ||
const consumer = new BlacConsumer([global, listener]); | ||
consumer.addLocalBloc({ bloc: local, id: '909' }); // should not trigger listener "global" | ||
@@ -198,3 +198,3 @@ expect(notify).toHaveBeenCalledTimes(0); | ||
const listener = new ValueChangeListener(notify, Test1, "all"); | ||
const consumer = new BlocConsumer([global, listener]); | ||
const consumer = new BlacConsumer([global, listener]); | ||
consumer.addLocalBloc({ bloc: local, id: '909' }); // should trigger listener "all" | ||
@@ -214,3 +214,3 @@ global.increment(); // should trigger listener "all" | ||
const one = new Test1(); | ||
const consumer = new BlocConsumer([]); | ||
const consumer = new BlacConsumer([]); | ||
consumer.addLocalBloc({ bloc: one, id: '909' }); | ||
@@ -222,3 +222,3 @@ expect(consumer.getLocalBlocForProvider('909', Test1)).toBe(one); | ||
const one = new Test1(); | ||
const consumer = new BlocConsumer([]); | ||
const consumer = new BlacConsumer([]); | ||
consumer.addLocalBloc({ bloc: one, id: '909' }); | ||
@@ -233,3 +233,3 @@ expect(consumer.getLocalBlocForProvider('909', Test1)).toBe(one); | ||
const two = new TestBloc(); | ||
const consumer = new BlocConsumer([]); | ||
const consumer = new BlacConsumer([]); | ||
consumer.addLocalBloc({ bloc: one, id: '909' }); | ||
@@ -245,3 +245,3 @@ consumer.addLocalBloc({ bloc: two, id: '192', parent: '909' }); | ||
const two = new TestBloc(); | ||
const consumer = new BlocConsumer([]); | ||
const consumer = new BlacConsumer([]); | ||
consumer.addLocalBloc({ bloc: two, id: '909' }); | ||
@@ -257,3 +257,3 @@ consumer.addLocalBloc({ bloc: two, id: '192', parent: '909' }); | ||
const bloc = new Test1(); | ||
const consumer = new BlocConsumer([bloc]); | ||
const consumer = new BlacConsumer([bloc]); | ||
const out = consumer.getGlobalBloc(Test1); | ||
@@ -265,3 +265,3 @@ expect(out instanceof Test1).toBe(true); | ||
const bloc = new Test1(); | ||
const consumer = new BlocConsumer([]); | ||
const consumer = new BlacConsumer([]); | ||
consumer.mocksEnabled = true; | ||
@@ -277,3 +277,3 @@ consumer.addBlocMock(bloc); | ||
const bloc = new Test1(); | ||
const consumer = new BlocConsumer([]); | ||
const consumer = new BlacConsumer([]); | ||
consumer.mocksEnabled = true; | ||
@@ -287,3 +287,3 @@ consumer.addBlocMock(bloc); | ||
const bloc = new Test1(); | ||
const consumer = new BlocConsumer([]); | ||
const consumer = new BlacConsumer([]); | ||
consumer.mocksEnabled = false; | ||
@@ -290,0 +290,0 @@ consumer.addBlocMock(bloc); |
@@ -6,3 +6,3 @@ import BlocBase from "./BlocBase"; | ||
export interface ReactBlocOptions { | ||
/** Enables debugging which calls BlocReact.observer every time a Subject is updated. Defaults to false */ | ||
/** Enables debugging which calls Blac.observer every time a Subject is updated. Defaults to false */ | ||
debug?: boolean; | ||
@@ -36,3 +36,3 @@ } | ||
export class BlocConsumer { | ||
export class BlacConsumer { | ||
observer: BlocObserver; | ||
@@ -39,0 +39,0 @@ public mocksEnabled = false; |
import * as index from "./index"; | ||
describe("Index", () => { | ||
it("should export BlocReact", () => { | ||
expect(index).toHaveProperty("BlocReact"); | ||
it("should export Blac", () => { | ||
expect(index).toHaveProperty("BlacReact"); | ||
}); | ||
@@ -7,0 +7,0 @@ it("should export Cubit", () => { |
import Bloc from "./Bloc"; | ||
import Cubit from "./Cubit"; | ||
import { BlocReact } from "./react/BlocReact"; | ||
import { BlacReact } from "./react/BlacReact"; | ||
import BlocObserver from "./BlocObserver"; | ||
export { Bloc, Cubit, BlocReact, BlocObserver }; | ||
export { Bloc, Cubit, BlacReact, BlocObserver }; |
import CounterCubit from "./bloc/CounterCubit"; | ||
import PreferencesCubit from "./bloc/PreferencesCubit"; | ||
import AuthBloc from "./bloc/AuthBloc"; | ||
import { BlocReact } from "../lib"; | ||
import { BlacReact } from "../lib"; | ||
import Observer from "../devTools/observer"; | ||
// import Observer from "../../devtools/src/observer"; | ||
const state = new BlocReact( | ||
const state = new BlacReact( | ||
[new PreferencesCubit(), new AuthBloc(), new CounterCubit()], | ||
@@ -10,0 +10,0 @@ { observer: new Observer() } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
144699
3949
2