Beyond counters — Using recompose and RXJS 6 to build a (semi) complex UI
If you haven’t heard about reactive programming yet, I invite
you to pause and get acquainted.
This gist is a great place to get started. Reactive programming allows
your applications to react to events and data streams in real
time. This means you can skip life cycle methods like
componentDidMount
and
componentDidUpdate
when checking for changes in
your application state. If you are already familiar with
reactive programming, then hopefully this post can give you more
insights on how it can dovetail seamlessly with React using the
recompose library.
If you would like to skip to the end, the codesandbox is here
Task
While learning about reactive programming in the context of React, I found a lot of great material, but not many tutorials outside of simple data fetching and counting. Here we will build a set of three lists. The first list contains users, while the other two containers list their likes and dislikes.
Clicking on a user should then load their likes and dislikes list and change the URL to denote which user is currently selected. If no user is specified in the the URL, then the first user on the list should be chosen automatically. Likes and dislikes should also be able to be added, deleted, and edited.
Tools
The main tools we will be using are recompose and RXJS. Both are utility libraries that help you harness the power of reactive programming. I would suggest looking through the docs of both libraries to get acquainted with their usage, but I will also explain how to use them in the context of building a UI in React.
Why
UI design is supposed to feel snappy and reactive, and reactive programming allows us to change UI elements according to the flow of our data. Unlike in redux, we don’t have to update our data store, wait for the update to propagate, check to see if the update is finished, and finally update our UI to reflect the changes. With reactive programming, we can push changes to data streams and our components instantly react to these changes.
As our applications grow in size, so can the state management. The result is a large state object which needs to be traversed and updated even when you are only using a small part of it for the current page or view. In many cases, this data can be kept locally and can be ephemeral, no longer taking up space when the user navigates away from the page.
First steps
The first thing we need to do in this project is to set up
recompose to work with the observable configuration used by RXJS.
This is done by importing the function
setObservableConfig
from recompose, and passing it
the from
utility from rxjs.
Now that we have recompose configured to work with RXJS, we can
start making prop streams for our component to consume. Let’s
first start with a load
stream which will fetch our
users from the backend and pass the response — as well as any
other props received — to the next stream.
Let’s break this down line by line.
First, we are importing a few operators from rxjs, so let’s
discuss what they do and why we need them.
switchMap
and map
allow us to map values
to other functions and components. switchMap
takes an
observable and flattens it, allowing us to pass just its values to
the next function and not the observable itself.
The following operators, tap
,
catchError
, and startWith
, help us with
debugging and allows us to give the user information about the
state of the application. tap
takes and observable,
performs a side effect, and returns a copy of the original
observable.
Using tap
to debug observables
By using tap
, we can log out the response we receive
from the backend without having to worry about changing anything
in our stream. Using the startWith
and
catchError
operators help us communicate the status
of the application to users by giving us the ability to do
conditional rendering based on it’s current state. We will go into
the implementation of this later.
Now that we understand what the operators we are using are able to
do, let’s step through the load stream logic. You will notice that
the variable load
itself is the result of the
mapPropsStream
function taken from
recompose
this takes as an argument a function which
receives a stream of props (streams are denoted by the suffix
$
for clarity, but that does not have any syntactic
meaning) which can then be piped through our logical operators.
Here is where switchMap
becomes very useful. Since
React can’t render anything with an Observable object, we need to
flatten our props stream so that we get only the values of that
stream. Those values are the actual props we need to render our
component. So we flatten the stream and map it to our function
which fetches the users from the backend. You can use
props
here to pass any additional arguments to your
function that fetches the data, but since we’re faking it here, we
don’t need to pass anything. Finally, our request returns a
users
array which we then map to an object which
passes along the props
, users
, and
signals a successful request by setting status
to
'SUCCESS'
.
You may have noticed that we use tap
here as well to
call the function setUserList
, which is contained in
the props passed to the load stream. This, combined with the
ternary check of isEmpty(props.userList
will help us
prevent unnecessarily re-fetching the data. If we have the data,
we just want to return it without any manipulation.
Using streams to handle DOM events
Let’s create a second props stream which handles choosing a user from the list and setting them as a selected user:
Much like in our load
stream, we want to create a
stream of props using recompose’s
mapPropsStream
function. However, instead of directly
switchMapping the props, we want to create two variables — a
stream and a handler — using another utility function from
recompose, createEventHandler
.
createEventHandler
will, as the name implies, create
a handler and a corresponding stream which you can then assign to
React events such as onChange
, onClick
,
etc. The values from that event will then be passed into the
stream which you can then pipe, map, and whatever else you need.
In our case, we want to flatten both the incoming props stream and
the stream from the event and produce a single object.
Taking our props, we want to check to see if there is a user
specified in the URL. In this project,
react-router-dom
is being used so we find the user
param in props.match.params.user
. But where is the
Route
to pass the match
prop to this
stream?!? Don’t worry, we will get there, just know that for now,
selectUser
will be passed the props from
react-router’s Route
component. Now that we have our
user from the URL, we want to start our stream with either that
user, or the first user in our userList
. If we don’t
have the userList
yet, we just want to start with
null
. Then we simply pipe the
selectedUser
through the stream and return an object
which contains all props passed to selectUser
as well
as our userSelect
function.
In order to use the two prop stream we just created, we need to make a component which will take the props from both streams and render our list element
The component itself is quite simple, it just takes the props,
checks to see if the right user is in the URL, and if not, pushes
the correct user there. Next, it takes our userList
,
maps through it and returns a list item for each user in the list
and adds some conditional styling if the user is currently
selected. If the request is still ongoing, then it displays a
loading component. Notice that we pass the
userSelect
function to our list items to use as an
onClick
handler. The user that is passed to
userSelect
is then pushed to the
selectUser
stream.
In order for this component to have access to all these props,
it’s time for some functional programming beauty. Using the
compose
function from recompose, we create a new
element which streams all the props from both load
,
selectUser
, and react-router to the
IndexPage
element. It is here we will also create the
function setUserlist
and the
userList
prop we saw in our load
stream.
The functions withState
and
withHandlers
allow us to keep persistent data and
manipulate that data inside our streams.
withState
accepts three arguments, the name of the
item in state, the name of the function used to update the state,
and the inital state. withHandlers
receive the
function specified in withState
and can accept as
many handlers as you need. In our case, we just need one handler
which will take the users
array from our fetch
function and set the userList
state property to that
array. We also pass the withRouter
function so that
our component and event streams have access to router props such
as match
and history
.
Time to put the first steps together
Next Steps
Now that we have our list of users, it is time to create a
component which will display their lists of likes and dislikes.
Here, we can take advantage of more recompose helper functions,
withContext
and getContext
. These will
prevent us from being trapped in passed-down props hell. Since we
will be creating more handlers to update the list of users, there
will be many functions which we will want to pass down to our
components that display the likes and dislikes which could result
in something like this:
All of these props would then have to be continually passed down
until they reach the components that actually consume them. That
is crazy! By using withContext
and
getContext
, we can pass down the props we need and
consume them in any child of our IndexPage
component.
So, let’s create our handlers:
Here, are a few basic functions that will update our state after
making a request to a fictitious endpoint. We can now pass these
handlers and our selected user down to child components using
withContext
withContext
takes two arguments, the first being the
childContextTypes
, and the second being a function
which returns the props to be passed as context. In our case, we
want to pass the selected user and our state handler functions
down as context. We get these props by placing our
withContext
function as an argument of our
compose
function like this:
We are now able to consume these props as context in any child component!
Using context in child components
Let’s use the context we just created to display users’ likes and dislikes and add a simple interface to add and remove data.
The above component simply takes the user and their likes from
context and displays them in a list. This component also allows
users to delete a like by clicking on an icon which calls our
deleteUser
function we defined earlier. Let’s add a
modal that will allow users to add an item to the list by clicking
on the add
icon.
It’s time to create more DOM event driven streams. The first stream we want to create will be one to open and close the form modal:
Handling a toggle event is quite simple, we specify whether we
want the toggle to be on or off, and then use scan
—
a function that applies an accumulator over the stream ( similar
to Array.reduce ) — to toggle the state. We then return all passed
props as well as the current state of the modal and the handler
itself.
Dealing with text input is a bit trickier:
In the above snippet, we take the stream produced by our event
handler ( in this case typing in a text input ) and we create a
new stream composed of only the value of
event.target
. It is also important to note that we
must use startWith
here or else this stream will not
be subscribed to and our component which uses this stream will not
be rendered.
To access these functions in our List component, we must compose them together like so:
By using composable streams, we are able to create a nice modal form which takes text input and adds it to the list of user’s likes. Here is it’s parent component:
And the modal itself:
Doing the same for the user’s dislikes gives us two lists that can be modified by the user.
Wrap up
Observable streams are a great way of managing data flows in
components. While some of the things outlined here might be a
bit overkill, I wanted to illustrate that composable streams
work well in many use cases. Breaking up logic into streams
can help create separate and composable pieces of logic that
can stem from complicated UIs. Components that subscribe to
changes in the streams are able to react to one source of
truth rather than having to check for side-effects in
lifecycle methods such as componentDidUpdate
. I
think that they are a great way to remove some of the
anti-patterns we see in React components and to avoid the
performance costs of a large Redux store.