Using RxJS/BLoC as a state management solution for React
Karim Elghamry
Posted on November 25, 2022
Hello fellow programmers 👋. in this article, we will discuss how we would use the RxJS library to implement the BLoC pattern as a state management solution for a React project.
Table of Contents
- Prerequisite knowledge
- What is BLoC?
- Creating a Todo app using BLoC/RxJS
- Considerations and closing remarks
Prerequisite knowledge
Before going through the content of this article, it is assumed that the reader has prior basic knowledge in Rx, at least with observables and observers. The article will try to briefly go over these concepts, if needed.
What is BLoC?
BLoC, which stands for Business Logic Component, is a pattern created by Paolo Soares and Cong Hui from Google that aims to separate the business logic from the view in a given application. One of its primary goals is to make the business logic reusable and independent from the corresponding view. Initially, this pattern was introduced within the Flutter community, but throughout this article, we will take the concepts of BLoC and implement them in a React application. To start with, BLoC has these main rules:
Each BLoC should have a
dispose()
method to cleanup any ongoing streams.Any data coming in a BLoC or any change in state within a BLoC must be done through a method.
Any data coming out of a BLoC should be done through a stream/observable.
The following diagram illustrates how the view and a BLoC interacts with each other.
Creating a Todo app using BLoC/RxJS
Now that we've gone briefly over the concepts of the BLoC pattern, we will attempt to implement it in an example todo react application.
App initialization
First, lets initialize a react typescript project using vite
:
npm create vite@latest todo-bloc --template react-ts
this should create a folder called todo-bloc
. Navigate to that folder and install the dependencies:
cd todo-bloc && npm install
once done, we'll proceed by installing the RxJS package and the uuid package (we'll need it later):
npm install rxjs uuid
we can now run the app in dev mode:
npm run dev
Folder structure
In our simple todo app, we will create the following sub directories inside the src
directory:
src/
├───blocs/
├───components/
├───hooks/
└───models/
blocs: will encapsulate all of the reusable business logic components in our app.
components: will govern the view (UI) react components.
hooks: will hold all of our custom hooks.
models: will contains the interfaces and abstract classes that are shared across the app.
Creating the models
To simulate a real world application, we will start first by creating a Todo
interface, which all of the todo objects in our app will conform to. We'll create a Todo.ts
file under the models directory and add the following interface:
-
id
: to distinguish between different todo objects. -
text
: the actual content of the todo object. -
isDone
: to keep track of whether this specific todo is marked as done or not.
Secondly, we'll create the base bloc
interface which will include all common methods/properties shared between the BLoC's in our app. Ultimately, we'll only need a dispose()
method on the bloc
interface as previously mentioned. We'll proceed by creating a Bloc.ts
file under the models directory and adding the following interface:
-
dispose()
: a method invoked to close any opened streams and complete all observables within our BLoC.
Creating the BLoC's
Now that we have the interfaces in place, lets start by defining our business requirements for our app. Essentially, our app should:
- display todo items chronologically.
- provide a way to add a todo item.
- provide a way to mark todo items as finished.
First off, we'll create a TodoBloc
class that will implement the Bloc
interface that was previously imposed. Ultimately, this class will live in a file called TodoBloc.ts
under the blocs directory. To implement the first business requirement, we'll have to find a way to keep track of the current list of todo items. This is where Subject
from RxJS
comes into play. To recall, any data coming out of a BLoC should be through a stream/observable. To achieve that, we will create a BehaviourSubject
that will hold the most recent list of todos, and emit new items whenever the state changes:
_todos$
: is a private property of typeBehaviorSubject<Todo[]>
that will be responsible for holding ourTodo
objects.dispose()
: we close/cleanup the_todos$
subject in the previously defined dispose method.
you might now be wondering, how are we going to expose the _todos$
subject to the view if the field is marked as private? good question. This is a bit tricky, because we don't want to expose the whole Subject
to the view as we don't want the view to be emitting data directly to the Subject (we have to do it through a method as previously mentioned). Instead, we will create a getter for the _todos$
field that will only expose the Observable
side of our subject:
To cover the second business requirement, we will create a method called addTodo
, which will be responsible for taking in a Todo
object and adding it to the _todos$
subject:
Explanation: the previous method accepts a Todo
object as a param, gets the current list of todos from the _todos$
subject, concatenates the given todo to the list of todos, and finally pushes the new list of todos to the _todos$
subject.
To implement the final business requirement, we will add a toggleTodo
method, which will be responsible for marking the given Todo
item as done/undone:
Explanation: toggleTodo
accepts a Todo
object as a param, gets the current list of todos from the _todos$
subject, loops over the retrieved list and toggles the given Todo
item by id, and finally pushes the new todo list to the _todo$
subject.
The final TodoBloc
class should look like this:
Creating the hook
Now that we're done implementing our business logic, we need to start thinking of a way to connect our BLoC to the anticipated view in our app. We can facilitate this process by creating a custom hook that will have the following responsibilities:
accept an
Observable
of a generic type as a param.consume the values coming from the
Observable
by subscribing to it when the components mounts.preserve the state of the most recent value from the
Observable
and update the state each time a new value comes in.cleanup the subscription when the component unmounts.
Having these points in mind, we'll create a useSubscribe
hook under the hooks directory to fulfill these responsibilities:
Explanation: useSubscribe
accepts an Observable
of a generic type as a param. A state is created to store the most recent value emitted from the given observable using the useState
hook. We then use the useEffect
hook to subscribe to the given observable on component mount and update the state with the most recent value. We also add a cleanup callback that will unsubscribe from the observable once the components unmounts. Finally, we return a copy of the state to the view to be able to effectively use it in display.
Creating the view
Now that we have everything in place, let us create the view to demonstrate how we can tie these all pieces together. First, we create a simple TodoItem
component under our components directory to display a single todo item:
Explanation: the TodoItem
component accepts two params - todo
, which is the given Todo
object, and onToggle
callback, which will be triggered when the user marks the items as done/undone. The component displays the text property of the given todo in a li
tag, and style is rendered according to the isDone
property.
Finally, in our App.tsx
file, we will tie all the pieces together:
Explanation: we create an instance of the TodoBloc
outside of our App
component. We then consume the most recent value from the the TodoBloc
by using the previously defined useSubscribe
hook and passing the todos$
observable as a param. Now, the todo
variable will always hold the most recent list of todos, and will update accordingly. To give the user the ability to add new todo items, we create a simple form with a single controlled input
element and a submit button. On submit, the addTodo
method on the TodoBloc
will be invoked, and a new Todo
object will be pushed to the list of todos. Finally, to display the list of todo items, we map over the todos list and render a TodoItem
component for each given todo. Each TodoItem
will be provided an onToggle
callback that will invoke the toggleTodo
method on the TodoBloc
for each item, respectively.
The final result will look/behave as follows:
Considerations and closing remarks
As your application grows, your number of BLoC's will grow respectively and you'll need to find a way to manage them cleanly. Multiple view components may consume a single BLoC and you'll need to find a way to provide it to these components. One way to go about that is to use the context api to provide a single bloc to multiple components. You may also want to look into the Singleton design pattern to preserve a single copy of a given BLoC within your app. You would also want to manage the disposal of each BLoC when it is no longer needed in your app (in our todo app, we did not call dispose()
anywhere because the TodoBloc
has the same lifespan as our application). Nevertheless, be flexible and reasonable with your design and do not over engineer your solutions. Happy coding 🎉!
Posted on November 25, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 9, 2024
October 2, 2024