Originally posted here on 25/10/2018 with the title: Functional React. Is it possible? Exploring functional programming techniques in React using Higher Order Components. The article is presented unedited.
I found out about hooks shortly after publishing it. I've sinced moved on from the codebase talked about in this article - but it would be good to revisit these concepts and see how they hold up against hooks nowadays.
Warning — this post contains lots of code. If you’re triggered by seeing large blocks of javascript, please consult your loved ones, religious leaders and or a comfort puppy before proceeding.
Sorry for the clickbait-y title. Let me be more upfront with you.
Yes. Yes it is.
And I’d like to show you how. But first, a little background.
I started out with React in the second half of 2017. I was pretty green with web development, but something about React stuck with me — more so than any other framework.
I was able to learn bits and pieces of React at my previous job, but it was only proofs of concepts and new codebases that weren’t getting shipped anytime soon.
I was pretty hungry to work with React full time. Luckily, I found Drawboard and the rest is history.
A few weeks before starting, I was emailing with one of my then-future colleagues — the other front-end dev — and he gave me a list of technologies/frameworks that were being used on the web app.
Now initially, I’d never heard of recompose. I only knew vaguely what a HOC — Higher Order Component — was. And functional programming was something whispered in the dark hallways, propounded by the gods — an unattainable paradigm that I knew nothing about.
So, before I started at Drawboard, I was writing pretty typical React code. It looked a little bit like this.
Since then, everything has changed. It’s very infrequent that I have to use the class
keyword. Most of our view logic is composed of Higher Order Components and utility functions. Small building blocks that are easily grokked (understood) in isolation, but powerful and all-mighty upon composition.
Have you tried https://t.co/TpmZKQ0QBO?
— James Adams (@jamesadams0) October 12, 2018
We're down to 50 usages of the `class` keyword in a big codebase - it's abstracted a way a bit when necessary using HOCs, but it's super rare that you really need it.
Here's some code from a talk I gave recently:https://t.co/joeKbIA9Ru pic.twitter.com/0EWrHH3fJB
Before we dive into the magnificent rabbit-hole that is writing functional React — let’s start with a few explanations/definitions.
The order of a function refers to how deep it is. Your typical function is single order. Upon execution — with some stuff (arguments) or otherwise — it does a thing, and then finishes execution (by returning some stuff or nothing).
A higher order function behaves on a grander scale. It has loftier aspirations.
But this is too general. We’re talking React!
Hang on a second. Isn’t a component just an abstraction from a function?
This naturally leads us to the following:
It’s okay if this isn’t crystal clear yet. But persist, and I assure you you’ll be using higher order functions and components all over the place once you can grasp their POWER.
If you’ve jumped on the React/Redux bandwagon anytime in the last couple years — you’ve probably used higher order components without even realising it.
Here’s a common one:
connect
takes some arguments — mapState
and mapDispatch
— and spits out a higher order component. This HOC takes your TodoList
component, and spits out a new, enhanced component. In this sense, connect
behaves like a HOC factory.
Just to really send it home:
Enough talk! Let’s take a look at the same component written in two different styles.
Here is our Farm
Component, written as a class.
//------------------Written as a class---------------------//
class Farm extends React.Component {
constructor(props) {
super(props)
this.state = {
tools: ["hammer", "scythe", "sickle"],
}
}
componentDidMount() {
fetch("/tools")
.then(({ body: { tools } }) => this.setState(tools))
.catch(err =>
console.log("Oh no, your tools got lost. Here's why: ", err)
)
}
shouldComponentUpdate(nextProps, nextState) {
return nextState.tools !== this.state.tools
}
render() {
const { tools } = this.state
const { someProp, anotherProp } = this.props
if (!(tools && tools.length)) {
return null
}
return (
<div>
Here are all the tools in the farm:
<ul>
{tools.map((tool, index) => (
<li key={index}> {tool} </li>
))}
</ul>
{someProp} - {anotherProp}
</div>
)
}
}
I’ve exaggerated a bit to prove a point — how often are you actually implementing shouldComponentUpdate
?— but otherwise it’s a pretty typical component.
And this is the same component, written functionally. Note that I use some helpers from the aforementioned library, recompose
. I’ll explain exactly how recompose works later on.
import branch from "recompose/branch"
import withState from "recompose/withState"
import renderNothing from "recompose/renderNothing"
import onlyUpdateForKeys from "recompose/onlyUpdateForKeys"
import lifecycle from "out-of-scope-for-this-article-but-serves-same-purpose-as-recompose-lifecycle"
// And now the exact same thing, written functionally.
//---------------------------------------------------------------//
// Only the 'view' logic.
const BaseFarm = ({ someProp, anotherProp, tools }) => (
<div>
Here are all the tools in the farm:
<ul>
{tools.map((tool, index) => (
<li key={index}> {tool} </li>
))}
</ul>
{someProp} - {anotherProp}
</div>
)
// All the 'other' stuff.
const didMount = ({ setTools }) =>
fetch("/tools")
.then(({ body: { tools } }) => setTools(tools))
.catch(err => console.log("Oh no, your tools got lost. Here's why: ", err))
const shouldNotRender = ({ tools }) => !(tools && tools.length)
const Farm = withState("tools", "setTools", ["hammer", "scythe", "sickle"])(
// N.B: lifecycle isn't the one from recompose
lifecycle({ didMount })(
branch(
shouldNotRender,
renderNothing
)(onlyUpdateForKeys(["tools"])(BaseFarm))
)
)
Or even better, using recompose/compose
:
// Or better:
// same imports as before plus...
import compose from "recompose/compose"
const Farm = compose(
withState("tools", "setTools", ["hammer", "scythe", "sickle"]),
lifecycle({ didMount }),
branch(shouldNotRender, renderNothing),
onlyUpdateForKeys(["tools"])
)(BaseFarm)
// Alternatively.
const enhance = compose(
withState("tools", "setTools", ["hammer", "scythe", "sickle"]),
lifecycle({ didMount }),
branch(shouldNotRender, renderNothing),
onlyUpdateForKeys(["tools"])
)
// enhance is a HOC, composed of a few other HOCs.
// It takes a component (BaseFarm) and returns Farm.
const Farm = enhance(({ someProp, anotherProp, tools }) => (
<div>
Here are all the tools in the farm:
<ul>
{tools.map((tool, index) => (
<li key={index}> {tool} </li>
))}
</ul>
{someProp} - {anotherProp}
</div>
))
At first glance, the two different styles can seem extremely different — but they really do the same thing.
Let’s say you have a Workshop
component. And it’s used on a separate view or even in an entirely different application. You’d be able to ‘steal’ all the business logic from the Farm
component— you could pick and choose the smaller HOCs or grab the composed enhance
HOC — without even breaking a sweat.
How would you do this in typical, class-based components? You can’t. This is what makes writing React functionally with HOCs so damn awesome.
That’s taken directly from the README
. Simply put, it’s a useful collection of HOCs and helpers that blaze the trail for you to write high-quality, functional React code.
Let’s deep dive into some recompose
code. I promise it’s quite approachable — it isn’t doing anything mega funky under the hood.
import { createFactory } from "react"
const mapProps = propsMapper => BaseComponent => {
const factory = createFactory(BaseComponent)
return props => factory(propsMapper(props))
}
You can use mapProps
to map the props coming in to your component before they get there. Let’s say our props look like this initially:
{routerProps: {location, query, hash}, tools, ...props}
But your component doesn’t care where the routerProps
come from — it just needs them as they are. An example propsMapper
could be:
const propsMapper = ({ routerProps, ...props }) => ({
...routerProps,
...props,
})
Nothing crazy, it just spreads the routerProps
.
So to use mapProps
you’d do something like this:
const myMapProps = mapProps(propsMapper)
const myEnhancedComponent = mapProps(myBaseComponent)
// alternatively, all in one go:
const myEnhancedComponent = mapProps(propsMapper)(myBaseComponent)
I think you’re starting to get the hang of this whole higher order thing.
Now — recompose has some really useful HOCs. But they’re basic, general and won’t solve every problem specific to your application. No worries —you can and should roll your own Higher Order Components to suit your needs. We have hundreds in our codebase.
Here is an example of an HOC that I wrote called withStateOfTypeSet
.
It’s useful for avoiding reimplementation of the same logic across many components that have to store some local state which belongs in a Set. For those unfamiliar with how Sets behave in JS —check out the MDN docs.
import withState from "recompose/withState"
import compose from "recompose/compose"
import withHandlers from "recompose/withHandlers"
import stringCapitalize from "somewhere"
const addToSetFactory = (stateName, stateUpdaterName) => props => item =>
props[stateUpdaterName](new Set(props[stateName]).add(item))
const removeFromSetFactory = (stateName, stateUpdaterName) => props => item =>
props[stateName].delete(item) &&
props[stateUpdaterName](new Set(props[stateName]))
const defaultInitialState = () => new Set()
/**
* Puts a field in the state named stateName, and provides addToStateName
* and removeFromStateName methods to add/remove from the set.
*
* eg: stateName=selectedTags, stateUpdaterName=setSelectedTags
*
* The following props will get added:
*
* selectedTags, setSelectedTags, addToSelectedTags, removeFromSelectedTags
*
*/
export default (stateName, stateUpdaterName, initialState) =>
compose(
withState(stateName, stateUpdaterName, initialState || defaultInitialState),
withHandlers({
["addTo" + stringCapitalize(stateName)]: addToSetFactory(
stateName,
stateUpdaterName
),
["removeFrom" + stringCapitalize(stateName)]: removeFromSetFactory(
stateName,
stateUpdaterName
),
})
)
I’m hoping the code is pretty self-documenting. It’s a nifty HOC which abstracts away some of the funky stuff you have to do when initialising or adding/removing from a Set
. The only thing I should really explain here is that recompose/withHandlers
adds functions to your component that are called with the most up to date props. Check it out.
So far — I’ve only showed learning examples. Whilst cool — they don’t really show what functional React can look like when taken to the extreme.
Below is the default export for one of our biggest components. It has a lot of behaviour and is one of the uglier of the bunch. But all of its functionality is composed of many small buildings blocks. All the handlers
are tiny functions. And the view logic is a stateless functional component that does nothing besides declare how the component should look.
Now — I’ve written a whole chunk about the what. But let’s jump to the extensive and overwhelmingly positive why.
I won’t cover the benefits of writing functional code in general — of which there are many — as this article would turn into a sermon. But it’s certainly within scope to explore some of the React-specific upsides.
You can now share code between components without copy-pasting. This is probably the biggest win. Your codebase will be more succinct, you’ll only have to change business logic in one place rather than 10, and code will be infinitely more readable. Plus, it’s so much easier for new developers to grok smaller functions and components than to try and grok 500 line monsters.
The effect is compounding once your whole codebase follows the same patterns. Because as time progresses — as it inevitably does — you’ll find that you’ve already done this new thing that you’re trying to build. And you’ve written 90% of the HOCs you need! So it’s just plug and play and then you can ship! 🚢 (Kidding, but optimistic delusions aside, sometimes it actually feels like that.)
Often, components you end up crafting are composed of 1-many HOCs wrapping some generic component such as a modal, a button, or a link.
Earlier in the year I talked about my experiences integrating React Native into an existing iOS application. I also spoke about it at MelbJS — the slides are here. It’s something I’m really excited about. And one thing that always stands out to me as revolutionary is the ability to share code between the two applications.
I’ve set up the web repo as a submodule in the iOS project, so any code that we write for the web can easily be used in the iOS application. There are a lot of cool parts to this — the iOS application is even ‘repping’ the same data layer as the web! — and if you write your React like we do, then you can share component business logic cross-platform.
Let’s take a real world example of this. A few months ago we built out issue management in our web application. Think of it as Jira for architects/engineers to raise issues on drawings.
Isn’t there some rule that states that every software project that grows sufficiently large will eventually build its own implementation of Jira? 🤔
Anyway, the export for one of the core components, Issue
, looked a little bit like this:
Notice the red. We needed this same logic when I began building the same feature set in the iOS application in React Native. So a natural refactor followed:
And then I could use this same HOC to wire up the React Native Issue
component!
Again, not a crazy complicated example, but I hope you can see how these ideas can extend to entire features, codebases or even cross project like in this instance. I didn’t even have to go looking for this example! When I gave the talk that morphed into this write-up, I’d just finished building this feature — it’s pretty cool how easy it was to find examples of why functional programming in React is so damn awesome, just from my day-to-day work.
Our ability to experiment so successfully/quickly with React Native was entirely driven by our web codebase being structured in this manner.
You can actually go one step further and share view logic as well using react-native-web
or react-primitives
but I’ve yet to bring these ideas into our applications.
I’m not a massive fan of selling some methodology so enthusiastically whilst at the same time, entirely neglecting to cover any pitfalls.
Let’s explore a few downsides of writing all your code like this.
But, you could see this as a plus if you appreciate the ability to see which HOC added which props into the chain.
this
could be a bit yuck though.)I hope I’ve given a tasty enough selection of what’s possible when you write React functionally. And maybe a little bit of insight into the journey I’ve undertaken in the last year or so.
I’m a big fan of putting my work out in public. It could save someone a lot of time — or give another person an opportunity to explain to me why my views are wrong. Please get in touch if you have any questions/want to tell me why you think this is bad, why I’m wrong or that I smell. I promise not to yell functional programming terms at you.
I’ll just put all that angst into a curry.
Resources:
The content for this article was fuelled by a talk I gave at the August React Meetup in Melbourne. You can find the slides here.