DEV Community

Implement Flutter BloC pattern in React

Last week, I faced an IT layoff. After the initial shock, I've embraced the silver lining—now I have more time for side projects and can sharpen my skills in Backend, Frontend, and Flutter. Working for companies often means focusing on projects, but this setback has humbly opened up a window for personal growth.

After a few years immersed in Flutter projects, I've discovered the joy and speed it brings to developing both mobile and web apps. Currently, I'm diving into a side project, using React.js for the web's frontend, Nest.js, MongoDb, and GraphQL for the backend, and Flutter for mobile applications. It's an exhilarating tech mix!

Implementing the BloC pattern I use in Flutter into React.js might raise eyebrows, but for me, it's about consistency and speed in developing applications for both web and mobile. Why not use Flutter for the web, you ask? Simple—it's not quite ready for content-based web applications.

Let the coding party begin! 🚀 First things first, let's lay the foundation with the base BloC class. But before we dive in, stepping into the world of reactive programming requires a trusty reactive object. We're keeping it smooth and easy by leveraging the rxjs package instead of reinventing the wheel with our observable objects. Let's roll! 😎💻

// bloc_basic.ts
import { BehaviorSubject, Observer } from "rxjs";

export abstract class Bloc<T> {
    private _state: BehaviorSubject<T>;
    constructor(initState: T) {
        this._state = new BehaviorSubject<T>(initState);
    }

    public get state(): T {
        return this._state.getValue();
    }

    private get $() {
        return this._state.asObservable();
    }


    emit(updatedState: T) {
        this._state.next(updatedState);
    }

    dispose() {
        this._state.complete();
    }

    listen(listener: Partial<Observer<T>> | ((value: T) => void)) {
        const observer = this.$.subscribe(listener);
        return () => observer.unsubscribe();
    }
}

Enter fullscreen mode Exit fullscreen mode

This TypeScript code creates a class called Bloc, following the BloC pattern with "rxjs". It manages state using BehaviorSubject, and you can update the state with the emit method. The dispose method cleans up when needed. The listen method lets you watch for state changes easily. In short, it's a simple and effective way to handle state in applications.

Now that our BloC class is taking shape, let's add a dash of interactivity! We need a way to register the UI and keep tabs on state changes. Two key functions are on the menu for now: one to switch up the UI and another to listen and execute a function. This way, you can do more than just updating the UI – think notifications, and beyond! Let's make this BloC sizzle! 🔥💻

bloc_hooks.ts
import { useEffect, useState } from "react";
import { Bloc } from "./bloc_basic";


export const useBloc = <T>(bloc: Bloc<T>): T => {
    const [state, setState] = useState<T>(bloc.state);
    useEffect(() => {
        const unsubscribe = bloc.listen((val: T) => setState(val));

        return () => unsubscribe();
    }, [bloc]);

    return state;
}


export type BlocListenerFunction<T> = (state: T) => void;
export const useBlocListener = <T>(bloc: Bloc<T>, { listener }: { listener: BlocListenerFunction<T> }) => {
    useEffect(() => {
        const unsubscribe = bloc.listen((val: T) => listener(val));

        return () => unsubscribe();
    }, [bloc]);
}

Enter fullscreen mode Exit fullscreen mode

The useBloc hook takes a BloC instance, sets up a state with its initial value, and automatically updates the state whenever the BloC changes. It ensures cleanup by unsubscribing when the component unmounts. The second hook, useBlocListener, is for more custom use cases, allowing you to specify a listener function to run when the BloC state changes. It, too, takes care of subscribing and unsubscribing to prevent memory leaks. Together, these hooks simplify integrating BloCs into React components.

We are almost done with basics, just a Widget,... eee, I mean Component to give us access to the state, so we can react and update UI if needed.

// bloc_components
import { Fragment, ReactNode } from "react";
import { useBloc } from "./bloc_hooks";
import { Bloc } from "./bloc_basic";


export type BlocBuilderFunction<T> = (state: T) => ReactNode;
export function BlocProvider<B extends Bloc<S>, S>({ bloc, builder }: { bloc: B, builder: BlocBuilderFunction<S> }) {
  const state = useBloc(bloc);
  return (
    <Fragment>
      {
        builder(state)
      }
    </Fragment>
  );
}

Enter fullscreen mode Exit fullscreen mode

We are done with the basics. Now, let's create a small example, imagine we have a shopping list page, and we have a state that keeps track of the items and loading state.

//shopping_list_logic.ts

import { Bloc } from "../../utils/basic_bloc/bloc_basic";
import { ShoppingList, fakeShoppingLists } from "./models/shopping_list_model";

// To simulate a http call
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));

// state of the page including loading and data state
export class ShoppingListState {
  items: ShoppingList[];
  isLoading: boolean = false;

  constructor(state: ShoppingListState) {
    this.items = state.items;
    this.isLoading = state.isLoading;
    return this;
  }

  static init(): ShoppingListState {
    return new ShoppingListState({
      items: [],
      isLoading: false,
    });
  }
};


// logic behind the page
export class ShoppingListBloc extends Bloc<ShoppingListState> {
  constructor() {
    super(ShoppingListState.init());
  }


  async loadData() {
    this.emit({
      ...this.state,
      isLoading: true,
    });

    await sleep(4000);

    this.emit({
      ...this.state,
      isLoading: false,
      items: fakeShoppingLists,
    })
  }
}

Enter fullscreen mode Exit fullscreen mode

And the final component is to show the page and data.

import { CircularProgress, List } from "@mui/material";
import ShoppingListCard from "./components/shopping_list_card";
import { Fragment, useEffect } from "react";
import { BlocProvider } from "../../utils/basic_bloc/bloc_components";
import { ShoppingListBloc, ShoppingListState } from "./shopping_list_logic";
import { Center } from "../../utils/layout_components";
import { useSetTitle } from "../dashboard/dashboard_logic";


const bloc = new ShoppingListBloc();
export default function ShoppingListPage() {
  useEffect(() => {
    bloc.loadData();
    return;
  }, []);


  return (
    <Fragment>
      <BlocProvider<ShoppingListBloc, ShoppingListState>
        bloc={bloc}
        builder={(state) => {
          return <>
            {
              state.isLoading ?
                <Center>
                  <CircularProgress />
                </Center> :
                <List>
                  {state?.items.map((item, i) => (
                    <ShoppingListCard key={item.id + i} item={item} />
                  ))}
                </List>

            }
          </>
        }}
      ></BlocProvider>

    </Fragment>
  );
}

Enter fullscreen mode Exit fullscreen mode

Just a heads up, there's room for more enhancements in the pipeline, like leveraging dependency injection for BloC creation or introducing an extra hook for comparative builders. As I gear up for my next side project straddling both Flutter and React, I'm committed to refining and updating the repo. Also, I'm exploring the addition of some useful layout Widgets. Let's keep things sharp and streamlined! 🛠️👨‍💻

Repo: https://github.com/golkhandani/shop_wise/tree/main/web

Top comments (0)