VOLVER

Patrón BLoC con React Hooks

3 min de lectura

El Patrón BLoC ha sido diseñado por Paolo Soares y Cong Hui, de Google y presentado por primera vez durante la DartConf 2018 (23-24 de enero de 2018). Ver el vídeo en YouTube.

BLoC significa Business Logic Component (Componente de Lógica de Negocio). Inicialmente concebido para compartir código entre Flutter y Angular Dart, funciona independientemente de la plataforma: aplicación web, aplicación móvil o back-end.

Ofrece una alternativa al puerto de Redux para flutter usando streams de Dart. Usaremos Observables de RxJS, aunque xstream funciona igualmente bien.

En resumen, el BLoC:

  • contendrá lógica de negocio (idealmente en aplicaciones más grandes tendremos múltiples BLoCs)
  • dependerá exclusivamente del uso de Observables tanto para entrada (Observer) como para salida (Observable)
  • permanecerá independiente de la plataforma
  • permanecerá independiente del entorno

¿Cómo funciona BLoC?

Otros han explicado BLoC mejor que yo, así que cubriré solo lo básico.

Esquema BLoC

El BLoC mantiene la lógica de negocio; los componentes desconocen sus internos. Los componentes envían eventos al BLoC vía Observers y reciben notificaciones vía Observables.

Implementando el BLoC

Aquí está un BLoC de búsqueda básico en TypeScript usando 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 ? `Resultados para ${q}` : "Todos los resultados";
}),
);
}
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$ y preamble$ exponen valores asíncronos que cambian cuando query cambia.

query expone un Observer<string> para que los componentes añadan nuevos valores. Dentro de SearchBloc, _query$: BehaviorSubject<string> sirve como fuente del stream, y el constructor declara _results$ y _preamble$ para responder a _query$.

Usándolo en React

Para usarlo en React, crea una instancia del BLoC y compártela con los componentes hijos vía contexto de React.

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

Expónlo usando el proveedor de contexto:

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

El useEffect devuelve el método dispose, completando el observer cuando el componente se desmonta.

Publica cambios al BLoC desde el componente 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)}
/>
);
};

Obtenemos el BLoC vía useContext, luego useEffect publica cada cambio de consulta al BLoC.

Ahora 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>
);
};

Usamos useContext para obtener el BLoC, luego useEffect se suscribe a los cambios de results$ para actualizar el estado local. Devolver la suscripción cancela la suscripción cuando el componente se desmonta.

Pensamientos finales

El código final es directo con conocimiento básico de Observables y hooks. El código es legible y mantiene la lógica de negocio fuera de los componentes. Debemos recordar desuscribirnos de los observables y desechar el BLoC al desmontar, pero hooks personalizados como useBlocObservable y useBlocObserver podrían resolver esto. Planeo probarlo en un proyecto paralelo donde uso este patrón.