Go Functional!
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.
Rendering data
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?
A box calledĀ āCompā
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
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
.
The mapĀ function
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.
Safety First
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.
JSX Pipeline
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.
The concatĀ function
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.
Nasty concat
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:
Nice concat
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:
- We no longer need a separate
<div />
to wrap other JSX elements - Our
concat
is now associative. Huzzah!
Conclusion
With the final addition of a concat
method, we can take a step back to admire the beauty of our code.
But why?
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.
Props Proxy
This is the more conventional usage of an HOC and it allows:
- Props manipulation
- State abstraction
- Accessing the instance via Refs
- Wrapping the
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:
Inheritance Inversion
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 š
The End
⦠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:
- https://www.youtube.com/watch?v=SfWR3dKnFIo
- https://www.youtube.com/watch?v=JZSoPZUoR58
- https://creaturephil.github.io/posts/exploring-oh-composable-world/
Addendum: Class components
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?
Addendum: Adding stricterĀ types
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.