In this three-part series, I want to take a functional approach to building React applications.
There will be mathematical theory sprinkled throughout the series, and hopefully by the end of it you will pick up some useful techniques!
- Part 1 - Deconstructing the React Component
- Part 2 - The Reader monad and read-only context
- Part 3 - Functional state management with Reducer
In the previous post, we looked at React
through a more functional lens and rebuilt parts of the React component by using a View
box that contains the computation
Props -> Element.
There are two remaining React component API that I want to replicate: context and state. And in this post we will learn how to add a read-only context for our application using yet another box object.
So onwards and upwards!
Motivation behind context
Say that we have a copyright notice View as follows.
In order for this View to be used in our application, we will need to pass the props
year down from the root View.
As the number of props grow in the application, the more data we will have to manually pass down. Not only is this a tedious process, but it also means that the root View will need to take in every single prop as its own input just so they can be passed down to descendents.
But wait, there is a better way!
Meet the Reader
Let’s start by defining a new Reader box that contains a computation of the type
Context -> a. What is
It really could be anything, but for now we can focus on computations that return a View.
The Reader defines one function
runReader that takes in the value of the context, and returns the result of the
Now, we can use the Reader like this.
Here, the Reader’s computation returns a View, which is our previously defined
that has its input props contramapped from the Reader context. So, when we run the reader with a context value,
we get the value (View) out of the box, which we can fold down to get the final element.
Take a moment to let that sink in.
Now, just like we did with View previously, we can make the Reader a pointed functor.
Then, we can use
map to wrap the notice with a
But what else can Reader hold?
Remember that the Reader contains the computation
Context -> a, where
a can be anything?
So far we’ve returned a View from the computation, but it can really be any value, even a function!
Let’s look at a function that changes the color of an element within a View.
We can wrap this up in a Reader.
To use this Reader, we need to run it with a context, then apply it to an element in the View.
Rather tediously, we have to run both readers before can apply the
colorize function to the footer view.
Of course, there is a better way to do this. And that is with the
ap of a Reader will call its resulting function with the resulting value of another
Reader. We can visualize this like so.
This means that we can simplify our usage of
withColor to this.
A bit of theory: The Reader is now an Applicative in addition to being a functor.
A must obey the following laws.
A.of(x => x).ap(v) === v
A.of(f).ap(A.of(x)) === A.of(f(x))
u.ap(A.of(y)) === A.of(f => f(y)).ap(u)
For the Reader just substitute
Reader in the above laws.
We can verify that these laws do indeed hold in this test.
Chaining the Reader context
Let’s consider another view for displaying the page title.
Now, say we want to read the
title from the context, just like we did for the copyright notice.
But, how can we use both the
footer2 together? Maybe we
map over the View inside
This solution works, but isn’t ideal since running
combined.runReader returns yet another Reader.
Therefore, in order to get the resulting element out we will have to run the Reader twice!
Imagine having to use invoke
runReader once for each usage of the context. Such drudgery! Luckily
for us, there is a much better way to do this. We need to add the
chain function takes in a function
f, similar to
is a function that takes the result from the previous computation as the input, then
returns another Reader from it. And when the resulting Reader is run, the context will be
passed to all of the chained computations.
This allows us to easily combine the header and footer boxes.
Indeed, we now only need to run the Reader once.
A bit of theory: The Reader is now a Monad.
M must obey the following laws.
- Left identity:
M.of(a).chain(f) === f(a)
- Right identity:
m.chain(M.of) === m
m.chain(f).chain(g) === m.chain(x => f(x).chain(g))
M in our case is the Reader.
We can verify that these laws do indeed hold in this test.
But what is a monad exactly?
You can think of a monad as any object that implements the
chain function, and obeys the monad laws.
Note that you may see
flatMap being used instead of
chain in other libraries or articles. They are usually
the same as
chain, and can be treated as aliases. You might also note a similarity between
chain and the Promise
then method. They are indeed almost the same. However, in the case of Promises, the computation is not lazy, and has
an immediate effect when constructed, whereas the Reader does nothing until
runReader is called.
Why didn’t you just ask?
Let’s define a helper function for our Reader monad.
Now, we can start the chain by asking for the context.
We will also add a helper for this dot-chain sytle of code using generator functions.
Assembling all the pieces
Recall that the app from the previous post rendered a message along with some header and footer content.
Let’s use our new friend, the Reader monad, and provide some internationalization (i18n) support for that “Hello ___!” message.
We’ll start by defining a message bundle and a
Now, we can create context-aware functions using
Also, recall our three morpshisms on elements from the last post. We’ll use them in the app as well!
Finally, we can build our entire app as follows.
While we’re at it, let’s crank it up to eleven by styling the app.
When we render the app, this will appear on the screen.
The runnable source code for the full example can be found in this repository.
In this post, we learned about Applicatives and Monads and created the Reader to provide read-only context for our application.
In the next post we will wrap up this series by adding support for stateful computations. So stay tuned!
If you want additional resources on Monads, I recommend these.