So the big idea is that although React itself is declarative, vast majority of code that goes into making an app is imperative. This can be fixed (if you think it needs fixing) by mixing in more functional patterns.
What is react?
A JavaScript library for building user interfaces — reactjs.org
Although React is for building data view (or user interfaces), it must mingle with data, in some form, at times. And when it does, almost always the data is tightly coupled with the the interface code.
Wouldn’t it be great if we could just pack our views in a separate box and just sprinkle the data as and when needed?
This code is from the talk “Oh Composable World!” by Brian Lonsdorf. If you haven’t seen it yet, I encourage you do so before continuing.
Oh Composable World! @ 21:40
If you haven’t seen the video, here’s a brief explanation of the code you see:
The Comp
function takes in a react component (as the argument g
) and returns an object with methods fold
and contramap
.
The fold
method is the original component passed to Comp. This means that calling fold
is just like calling Foo
or rendering <Foo />
. After all, the following expressions give equivalent values.
<Foo fizz="buzz" />
Foo({ fizz: "buzz" })
Comp(Foo).fold({ fizz: "buzz" })
The contramap
method is there to modify props. It takes in a function f
that takes in some value and returns another value. The returned value is then fed into the original react component for rendering as a prop.
We can say that contramap
transforms the input to the component.
Modifying the output means to modify JSX. You may be thinking of a higher order component. But here’s the problem — HOCs don’t work with JSX.
Concretely, a higher-order component is a function that takes a component and returns a new component. — React docs on Higher order components
With the help of flow, if I create types for both Component and HOC, I get:
type **Component** = Props => Element;
type **HOC** = Component => Component;
It isn’t immediately obvious, but the type signature for an HOC is pretty long:type **HOC** = (Props => Element) => (Props => Element);
What we want is fairly simple: Element => Element
.
This is what the map
function is used for. If contramap
deals with a component’s input (props) then map
deals with its output (JSX). The function f
takes some JSX and wraps the original component’s output in it.
Before venturing any further, we need to make the code safe — type safe.
Here are the basic flow types:
The important thing to observe here is that the type Component
is a mapping from type Props
to type Element
.
Armed with types, we can now annotate our Comp
function’s return object. Please note that I am using Flow’ [$Exact](https://flow.org/en/docs/types/utilities/#toc-exact)
utility type to make the intent clear that type Box
is an object with fixed keys and values.
If we really think about it, we can see that the function Comp
is a mapping from type Component
to type Box
.
If you’re not a fan of arrow functions, you can rewrite the same thing as a function declaration. I prefer the current system because I can decouple my types from my code. IMO, that’s much cleaner.
Writing Comp
using function declaration means we’d have to integrate the types into the function definition.
Right now, every Box
object sits in isolation from the rest of the world. They can’t even connect with each. But if they could, we’d have an extensible JSX pipeline at our hands.
Conceptually, the concat
function merges two entities of the same type. We should also add a concat
method to our Box
type.
With the help of our new friend, we can now join multiple Box
es together.
It works nicely but there’s a small hiccup. Everytime concat
is called, it appends a new <div />
into the markup. And that’s nasty.
An array equivalent would look something like:[1].concat([2]) === [1, [2]]
But we know that’s not how concat
works on arrays. It works like:[1].concat([2]) === [1, 2]
This means that array’s concat
method is associative:[1].concat([2].concat(3)) === [1].concat([2]).concat(3) === [1, 2, 3]
But our concat
is not associative:
The easy way out is to wrap the result of concat in an array, since arrays already have a nice and friendly concat
method, and also because React 16 added support for rendering an array of JSX elements. But doing so means rewriting the Comp
function. Besides, it’s already been covered in “Deconstructing the React Component” by Jack Hsu.
Instead, we will use the [<Fragment />](https://reactjs.org/blog/2017/11/28/react-v16.2.0-fragment-support.html#what-are-fragments)
component. If you’re feeling adventurous, you can use the updated syntax for fragments.
With such a minor update to the code, we’ve solved two problems:
<div />
to wrap other JSX elementsconcat
is now associative. Huzzah!
With the final addition of a concat
method, we can take a step back to admire the beauty of our code.
Without going any further than this, it’s best if we talk about the merits of our current system. In the article “React Higher Order Components in depth”, Fran Guijarro has outlined numerous use cases for HOCs and split them up in two classes of implementation — Props Proxy and Inheritance Inversion. We’ll look at each of them and see what we can do about them.
This is the more conventional usage of an HOC and it allows:
WrappedComponent
with other elements
Props manipulation and **State abstraction**I’ll have you know that there’s yet another pattern that deals with this specific usage of HOCs. It’s called Render Props and you can read more about it in my other article “Exploring Render Props”.
Let’s look at some sample code that uses a HOC to implement both usages:
Before we can make the code functional, we need to convert it to the render props pattern.
We can now functional-ise the code. First, we pack <Num />
and <Foo />
in separate Box
es. Since Num
is a class, we also need a function to convert ES6 classes into functions. Luckily, Brian Lonsdorf got us covered.
And now, we define our <App />
in terms of Box
es.
And that’s it. Two HOC uses down, a few more to go.
Accessing the instance via RefsTypically, this is how we use refs in HOCs:
Which is, under the hood, just a nested component:
And the functional equivalent is:
Wrapping the **WrappedComponent**
with other elementsOkay. This one is pretty obvious. We just wrap one component in another. This is exactly what the map
function does:
We have covered the entirety of what an HOC can do by acting like a component pass through. If you thought that was interesting, just check out how to implement II — it’s bound to confuse you. An HOC that returns a new component that extends WrappedComponent
.
This is the one thing that we can’t do with with Box
es. We cannot have inheritance amongst functions and functions are a major participant in our functional pattern. And this makes me sad 😔
… for now. I hope you found this bit interesting and possibly even helpful. Now we can explore data flow in our functional react pattern. But that’s an affair for another time. The code for this post is on repl.it.
If you want more, here’s a list for you:
So far, we’ve only looked at functions and ignored class components. Let’s invite them to the party as well. Since Comp
can only handle functions, we will need a helper function that converts classes to functions.
That’s all we needed to do. Seeing how simple of our helper classToFn
is, can you make a guess at its type? Made your guess? Ready for the answer?
Once you’re done with this article, I suggest you take a look at this article to better explore the landscape of type safety that comes with this kind of setup.