TORNAR

Patró BLoC amb React Hooks

4 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 va ser concebut per permetre reutilitzar el mateix codi entre Flutter i Angular Dart, però de fet és independent de la plataforma: aplicació web, aplicació mòbil, back-end.

Pot considerar-se una alternativa al port de Redux per a flutter fent ús de streams de Dart. En el nostre cas, utilitzarem Observables de la llibreria RxJS , però qualsevol altra opció com xstream serà vàlida també.

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?

No explicaré extensament com funciona BLoC (hi ha altres persones que van fer una millor feina de la que faré aquí), sinó només algunes pistes bàsiques.

Esquema BLoC

El BLoC mantindrà la lògica de negoci i els components no tindran coneixement sobre el que està succeint dins. Els components enviaran esdeveniments al BLoC via _Observers_ i seran notificats pel BLoC via Observables.

Implementant el BLoC

Aquest és un exemple bàsic en typescript d'un BLoC de cerca 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$ estan exposats per ser subscrits des d'un component i expressen valors asíncrons que canvien en resposta a canvis en query.

query s'exposa com Observer<string> a l'exterior, per permetre l'addició de nou valor des dels components. Dins de SearchBloc, tenim _query$: BehaviorSubject<string> com la font del stream, i el constructor declara _results$ i _preamble$ per respondre a _query$.

Utilitzant-lo en React

Per utilitzar-lo en React necessitem crear una nova instància del BLoC i compartir-la als components fills utilitzant un context de React.

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

Hem d'exposar-lo utilitzant el proveïdor de context:

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

És important tenir el useEffect retornant el mètode dispose del BLoC perquè completi l'observer quan el component es desmunti.

Després podem publicar 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)}
/>
);
};

Vam obtenir el BLoC utilitzant el hook useContext i després amb useEffect cada vegada que la consulta canvia publiquem el nou valor al BLoC.

Ara és el torn de 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>
);
};

Com abans, utilitzem el useContext per obtenir el BLoC, però ara en el useEffect ens subscrivim als canvis en la interfície results$ per actualitzar l'estat local del component. És important retornar el valor de retorn de la subscripció, ja que es dessubscriurà quan el component es desmunti.

Pensaments finals

El codi final no és complex, almenys si tens un coneixement mínim sobre Observables i hooks. Tinc la sensació que el codi és bastant llegible i ajuda a mantenir la lògica de negoci fora dels components. És cert que hauríem de tenir cura de dessubscriure'ns dels observables i rebutjar el BLoC quan els components es desmunten, però aquests problemes podrien ser fàcilment solucionables creant alguns nous hooks com useBlocObservable i useBlocObserver. Però això ho intentaré en el futur, aviat espero, en un projecte paral·lel on estic utilitzant aquest patró.