TORNAR

Patró BLoC amb React Hooks

3 min de lectura

El Patró BLoC ha estat dissenyat per Paolo Soares i Cong Hui, de Google i presentat per primera vegada durant la DartConf 2018 (23-24 de gener de 2018). Veure el vídeo a YouTube.

BLoC significa Business Logic Component (Component de Lògica de Negoci). Inicialment concebut per compartir codi entre Flutter i Angular Dart, funciona independentment de la plataforma: aplicació web, aplicació mòbil o back-end.

Ofereix una alternativa al port de Redux per a flutter utilitzant streams de Dart. Utilitzarem Observables de RxJS, tot i que xstream funciona igualment bé.

En resum, el BLoC:

  • contindrà lògica de negoci (idealment en aplicacions més grans tindrem múltiples BLoCs)
  • dependrà exclusivament de l'ús d'Observables tant per a entrada (Observer) com per a sortida (Observable)
  • romandrà independent de la plataforma
  • romandrà independent de l'entorn

Com funciona BLoC?

Altres han explicat BLoC millor que jo, així que cobriré només el bàsic.

Esquema BLoC

El BLoC manté la lògica de negoci; els components desconeixen els seus internals. Els components envien esdeveniments al BLoC via Observers i reben notificacions via Observables.

Implementant el BLoC

Aquí està un BLoC de cerca bàsic en TypeScript utilitzant RxJS:

export class SearchBloc {
private _results$: Observable<string[]>;
private _preamble$: Observable<string>;
private _query$ = new BehaviorSubject<string>("");
constructor(private api: API) {
this._results$ = this._query$.pipe(
switchMap((query) => {
return observableFrom(this.api.search(query));
}),
);
this._preamble$ = this.results$.pipe(
withLatestFrom(this._query$, (_, q) => {
return q ? `Resultats per a ${q}` : "Tots els resultats";
}),
);
}
get results$(): Observable<string[]> {
return this._results$;
}
get preamble$(): Observable<string> {
return this._preamble$;
}
get query(): Observer<string> {
return this._query$;
}
dispose() {
this._query$.complete();
}
}

results$ i preamble$ exposen valors asíncrons que canvien quan query canvia.

query exposa un Observer<string> perquè els components afegeixin nous valors. Dins de SearchBloc, _query$: BehaviorSubject<string> serveix com a font del stream, i el constructor declara _results$ i _preamble$ per respondre a _query$.

Utilitzant-lo en React

Per utilitzar-lo en React, crea una instància del BLoC i comparteix-la amb els components fills via context de React.

const searchBloc = new SearchBloc(new API());
const SearchContext = React.createContext(searchBloc);

Exposa'l utilitzant el proveïdor de context:

const App = () => {
const searchBloc = useContext(SearchContext);
useEffect(() => {
return searchBloc.dispose;
}, [searchBloc]);
return (
<SearchContext.Provider>
<SearchInput />
<ResultList />
</SearchContext.Provider>
);
};

El useEffect retorna el mètode dispose, completant l'observer quan el component es desmunta.

Publica canvis al BLoC des del component SearchInput:

const SearchInput = () => {
const searchBloc = useContext(SearchContext);
const [query, setQuery] = useState("");
useEffect(() => {
searchBloc.query.next(query);
}, [searchBloc, query]);
return (
<input
type="text"
name="Search"
value={query}
onChange={({ target }) => setQuery(target.value)}
/>
);
};

Obtenim el BLoC via useContext, després useEffect publica cada canvi de consulta al BLoC.

Ara el ResultList:

const ResultList = () => {
const searchBloc = useContext(SearchContext);
const [results, setResults] = useState([]);
useEffect(() => {
return searchBloc.results$.subscribe(setResults);
}, [searchBloc]);
return (
<div>
{results.map(({ id, name }) => (
<div key={id}>{name}</div>
))}
</div>
);
};

Utilitzem useContext per obtenir el BLoC, després useEffect es subscriu als canvis de results$ per actualitzar l'estat local. Retornar la subscripció cancel·la la subscripció quan el component es desmunta.

Pensaments finals

El codi final és directe amb coneixement bàsic d'Observables i hooks. El codi és llegible i manté la lògica de negoci fora dels components. Hem de recordar dessubscriure'ns dels observables i descartar el BLoC al desmuntar, però hooks personalitzats com useBlocObservable i useBlocObserver podrien resoldre això. Planejo provar-ho en un projecte paral·lel on utilitzo aquest patró.