Now Available React Programming: The Big Nerd Ranch Guide
Front-End ReactBased on our React Essentials course, this book uses hands-on examples to guide you step by step through building a starter app and a complete,...
React is pretty awesome, and with stateless functional components you can create ambitious apps that are 98% plain ol’ JavaScript (optionally JSX), and are very lightly coupled to the framework.
Minimizing the surface area between React and your codebase has amazing benefits:
There’s an important catch to stateless functional components: you can’t use state or lifecycle hooks. However, this design encourages component purity and makes it trivial to test our components — after all, it’s just a function that maps data to virtual DOM!
“Great, but I’m not building a static page — I need state, so I can’t use stateless functional components!”
In a well-written React app, stateless functional components will cover most of your UI code, but an app’s complexity typically relates to state management. To help bug-proof the remainder of our codebase, we are going to turn class-based React Components into stateless functional components with functional programming and higher-order components (HOC) to isolate state from our pure components.
If you aren’t familiar with higher-order components, you may want to check out the official React guides first.
Why will destroying all classes with functional programming and higher-order components improve your codebase?
Imagine an app where all state is isolated, the rest of your app is a pure function of that state, and each layer of your component tree is trivial to debug directly from the React DevTools. Relish the thought of reliable hot module reloading in your React Native app.
Higher-order components are the ultimate incarnation of composition over inheritance, and in the process of turning our class components inside-out, subtle dependencies and nasty bugs pop right to the surface.
By avoiding classes, we can prevent a super common source of bugs: hidden state. We’ll also find testing gets easier as the software boundaries become self-evident.
Because higher-order components add behavior through composition, you can reuse complex state logic across different UIs and test it in isolation! For example, you can share a data fetching higher-order component between your React web app and React Native app.
Let’s look at a real-world example from a React Native project. The VideoPage
component is a screen in the mobile app that fetches videos from a backend API and displays them as a list. The component has been tidied up a bit to remove distractions but is unchanged structurally.
import React, { Component } from 'react' import { ScrollView, Text, View } from 'react-native' import Loading from 'components/loading' import Video from 'components/video' import API from 'services/api' class VideoPage extends Component { constructor(props) { super(props) this.state = { data: null } } async fetchData(id) { let res = await API.getVideos(id) let json = await res.json() this.setState({ data: json.videos }) } componentWillMount() { this.fetchData(this.props.id) } renderVideo(video) { return ( <Video key={video.id} data={video} /> ) } renderVideoList() { if (this.state.data.videos.length > 0) { return this.state.data.videos.map(video => this.renderVideo(video) ) } else { return ( <View> <Text>No videos found</Text> </View> ) } } buildPage() { if (this.state.data) { return ( <ScrollView> <View> <Text>{this.state.data.title}</Text> { this.state.data.description ? <Text>{this.state.data.description}</Text> : null } </View> <View> {this.renderVideoList()} </View> </ScrollView> ) } else { return <Loading /> } } render() { return this.buildPage() } } export default VideoPage
At 65 lines of code, the VideoPage
component is pretty simple but hides a lot of edge cases. Although there’s some syntactic noise that could be removed to bring down the line count a bit, the deeper issue is the high branching complexity and conflation of responsibilities. This single component fetches data, branches on load status and video count, and renders the list of videos. It’s tricky to test these behaviors and views in isolation, extract behaviors (like data fetching) for reuse or add performance optimizations.
Rather than jump to the end solution, it’s more instructive to see the process. Here’s our five-step roadmap to turn VideoPage
inside out and destroy all classes!
enhance()
functionOur first step is to cut down on instance methods, so let’s start by extracting .buildPage()
, .renderVideo()
and .renderVideoList()
from the VideoPage
class and make them top-level functions.
class VideoPage extends Component { ... - renderVideo(video) { - ... - } - renderVideoList() { - ... - } - buildPage() { - ... - } ... } +let renderVideo = video => { + ... +} +let renderVideoList = () => { + ... +} +let buildPage = () => { + ... +}
Hmm, those look like components now! Let’s rename renderVideoList()
and inline renderVideo()
.
-let renderVideo = video => { ... } -let renderVideoList = () => { +let VideoList = () => { if (this.state.data.videos.length > 0) { return this.state.data.videos.map(video => - this.renderVideo(video) + <Video key={video.id} data={video} /> ) } else {
Now that the new VideoList
component doesn’t have access to this
, we need to directly pass the data it needs as props. A quick scan through the code shows we just need the list of videos
.
-let VideoList = () => { +let VideoList = ({ videos }) => { - if (this.state.data.videos.length > 0) { + if (videos.length > 0) { - return this.state.data.videos.map(video => + return videos.map(video =>
Hey look, we have a pure component now! Let’s do the same to buildPage()
, which is really the heart of the VideoPage
component.
-let buildPage = () => { +let VideoPage = ({ data }) => { - if (this.state.data) { + if (data) { return ( <ScrollView> <View> - <Text>{this.state.data.title}</Text> + <Text>{data.title}</Text> - { this.state.data.description ? <Text>{this.state.data.description}</Text> : null } + { data.description ? <Text>{data.description}</Text> : null } </View> <View> - {this.renderVideoList()} + <VideoList videos={data.videos} /> </View> </ScrollView> )
To finish wiring things up, let’s rename the original VideoPage
class component to VideoPageContainer
and change the render()
method to return our new stateless functional VideoPage
component.
-class VideoPage extends Component { +class VideoPageContainer extends Component { ... render() { - return this.buildPage() + return <VideoPage data={this.state.data} /> } } -export default VideoPage +export default VideoPageContainer
So far, here’s what we have:
import React, { Component } from 'react' import { ScrollView, Text, View } from 'react-native' import Loading from 'components/loading' import Video from 'components/video' import API from 'services/api' class VideoPageContainer extends Component { constructor(props) { super(props) this.state = { data: null } } async fetchData(id) { let res = await API.getVideos(id) let json = await res.json() this.setState({ data: json.videos }) } componentWillMount() { this.fetchData(this.props.id) } render() { return <VideoPage data={this.state.data} /> } } let VideoList = ({ videos }) => { if (videos.length > 0) { return videos.map(video => <Video key={video.id} data={video} /> ) } else { return ( <View> <Text>No videos found</Text> </View> ) } } let VideoPage = ({ data }) => { if (data) { return ( <ScrollView> <View> <Text>{data.title}</Text> { data.description ? <Text>{data.description}</Text> : null } </View> <View> <VideoList videos={data.videos} /> </View> </ScrollView> ) } else { return <Loading /> } } export default VideoPageContainer
We have successfully split the monolithic VideoPage
component into several subcomponents, most of which are pure and stateless. This dichotomy of smart vs. dumb components will set the stage nicely for further refactoring.
What about the remaining instance methods? Let’s move the .fetchData()
method outside the class to a top-level function and rewire componentDidMount()
to invoke it.
- componentWillMount() { + async componentWillMount() { - this.fetchData(this.props.id) + this.setState({ data: await model(this.props) }) } } ... -async fetchData(id) { +let model = async ({ id }) => { let res = await API.getVideos(id) let json = await res.json() - this.setState({ data: json.videos }) + return json.videos }
Since we need the lifecycle hook to instantiate data fetching, we can’t pull out the .componentWillMount()
method, but at least the logic for how to fetch the data is extracted.
The VideoList
component could stand to be broken down into subcomponents so it’s easier to debug the if
branches. Let’s extract the two cases into their own stateless functional components:
+let VideoListBase = ({ videos }) => + <View> + { videos.map(video => + <Video key={video.id} data={video} /> + ) } + </View> + +let NoVideosFound = () => + <View> + <Text>No videos found</Text> + </View> + let VideoList = ({ videos }) => { if (videos.length > 0) { - return videos.map(video => - <Video key={video.id} data={video} /> - ) + return <VideoListBase videos={videos} /> } else { - return ( - <View> - <Text>No videos found</Text> - </View> - ) + return <NoVideosFound /> } }
Hmm, the current VideoList
component is nothing more than an if
statement, which is a common component behavior. And thanks to functional programming, behaviors are easy to reuse through higher-order components.
There’s a great library for reusable behavior like branching: Recompose. It’s a lightly coupled utility library for creating higher-order components (which are really just higher-order functions).
Let’s replace VideoList
with the branch
higher-order component.
+import { branch, renderComponent } from 'recompose' -let VideoList = ({ videos }) => { - if (videos.length > 0) { - return <VideoListBase videos={videos} /> - } else { - return <NoVideosFound /> - } -} +let VideoList = branch( + ({ videos }) => videos.length === 0, + renderComponent(NoVideosFound) +)(VideoListBase)
When there are no videos, the branch()
higher-order component will render the NoVideosFound
component. Otherwise, it will render VideoListBase
.
A higher-order component is usually curried. The first invocation accepts any number of configuration arguments — like a test function — and the second invocation accepts only one argument: the base component to wrap. Currying doesn’t seem to gain us anything yet, but later when we stack several higher-order components together, the currying convention will save us some boilerplate and make testing really elegant.
Take a look at some of these Recompose recipes for more inspiration.
We’re nearly done! VideoPageContainer
is now a generic, reusable “smart component” that fetches data asynchronously and passes it as a prop to another component. Let’s turn VideoPageContainer
into our own higher-order component, called withModel()
:
+let withModel = (model, initial) => BaseComponent => - class VideoPageContainer extends Component { + class WithModel extends Component { constructor(props) { super(props) - this.state = { data: null } + this.state = { data: initial } } ... render() { - return <VideoPage data={this.state.data} /> + return <BaseComponent data={this.state.data} /> } } }
The function signature of withModel()
indicates that the first invocation should provide a function for fetching the necessary data, followed by an initial value for the data while it is loading. The second invocation takes the component to wrap, and returns a brand new component with data fetching behavior.
To use withModel()
, let’s invoke it with the VideoPage
stateless functional component and export the result.
-export default VideoPageContainer +export default withModel(model, null)(VideoPage)
The withModel()
higher-order component will definitely be useful for other components in the app, so it should be moved to its own file!
enhance()
functionCurrying the withModel()
higher-order component has an elegant benefit: we can stack more “behaviors” with Recompose utilities! Similar to our work with the VideoList
and NoVideosFound
components, let’s extract the if (data)
edge cases from VideoPage
with the branch()
higher-order component to render the Loading
component while the data is being fetched:
-import { branch, renderComponent } from 'recompose' +import { branch, renderComponent, compose } from 'recompose' ... -let VideoPage = ({ data }) => { +let VideoPage = ({ data }) => - if (data) { - return ( <ScrollView> ... </ScrollView> - ) - } else { - return <Loading /> - } -} +export let enhance = compose( + withModel(model, null), + branch( + ({ data }) => !data, + renderComponent(Loading) + ) +) -export default withModel(model, null)(VideoPage) +export default enhance(VideoPage)
The compose()
utility saves us from deeply nested parentheses and linearizes stacked behaviors into a single function, conventionally called enhance()
. Hurray for clean git diff
s!
And now the VideoPage
“dumb component” focuses solely on the happy path: when there is data and at least one video to display. By reading the enhance
function from top to bottom, we can quickly parse out other behaviors or even add new ones, e.g. performance optimizations with onlyUpdateForKeys()
.
After a few more tweaks, here is the completed VideoPage
component in 52 lines of code (also on Github):
import React from 'react' import { ScrollView, Text, View } from 'react-native' import { compose, branch, renderComponent } from 'recompose' import Loading from 'components/loading' import Video from 'components/video' import API from 'services/api' import withModel from 'lib/with-model' let VideoPage = ({ data }) => <ScrollView> <View> <Text>{data.title}</Text> { data.description ? <Text>{data.description}</Text> : null } </View> <View> <VideoList videos={data.videos} /> </View> </ScrollView> let VideoListBase = ({ videos }) => <View> { videos.map(video => <Video key={video.id} data={video} /> ) } </View> let NoVideosFound = () => <View> <Text>No videos found</Text> </View> let VideoList = branch( ({ videos }) => videos.length === 0, renderComponent(NoVideosFound) )(VideoListBase) let model = async ({ id }) => { let res = await API.getVideos(id) let json = await res.json() return json.videos } export let enhance = compose( withModel(model, null), branch( ({ data }) => !data, renderComponent(Loading) ) ) export default enhance(VideoPage)
Not bad! At a glance, we can see the happy path for rendering VideoPage
, how it fetches data, and how it handles the load state. When we add new behaviors in the future, we will only add new code instead of modifying existing code. So in a way, functional programming helps you write immutable code!
Interestingly, every component and function (except model()
) is an arrow function with an implied return. This isn’t just about syntactic noise: the implied return makes it harder to sneak in side effects! The code looks like a strict “data in, data out” pipeline. The implied return also discourages you from assigning to local variables, so it is hard for ugly interfaces to hide when all destructuring must happen in the parameter list. And to add impure behaviors like performance optimization or handlers, you are naturally forced to use higher-order components.
We can even test the component’s enhance
r in isolation by stubbing out the VideoPage
component:
import { enhance } from 'components/video-page' it('renders when there is data', () => { let Stub = () => <a>TDD FTW</a> let Enhanced = enhance(Stub) /* Perform assertions! */ })
Back when rendering was tangled up in instance methods, our only hope of extracting behaviors was through inheritance, e.g. mixins. But now we can reuse behaviors through straightforward function composition. The inside-out transformation also highlights that VideoList
should be extracted to its own module, video-list.js
.
Functional programming recipes and patterns go a long way to creating elegant, resilient, and test-friendly code by minimizing the surface area between our code and the framework. Whether you are creating a React web app or React Native app, higher-order components are a particularly powerful technique because they encourage composition over inheritance.
With functional programming, we can build React components that resemble a tasty sandwich, where we can peel back each ingredient and debug layer-by-layer. By contrast, class-based components are a burrito wrap with potato salad.
Interested in learning next-generation JavaScript for the web platform? Join us for a Front-End Essentials bootcamp, or we’ll come to you through our team training program.
Based on our React Essentials course, this book uses hands-on examples to guide you step by step through building a starter app and a complete,...
Svelte is a great front-end Javascript framework that offers a unique approach to the complexity of front-end systems. It claims to differentiate itself from...
Large organizations with multiple software development departments may find themselves supporting multiple web frameworks across the organization. This can make it challenging to keep...