DESTROY ALL CLASSES: Turn React Components Inside Out with Functional Programming

React July 19, 2017

This post first appeared on the Big Nerd Ranch blog.

A real-world example of refactoring a class-based component from a React Native app into stateless functional components and higher-order components, in 5 steps.

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:

  1. Framework updates will have little effect on your code.
  2. You can easily write isolated unit tests, instead of UI integration tests.

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.

What are the benefits?

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.

Example: Refactoring a React Native component

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!

  1. Turn instance methods into stateless functional components
  2. Extract remaining instance methods to plain functions
  3. Extract branching complexity with higher-order components
  4. Create a data fetching higher-order component
  5. Compose behaviors into a single enhance() function

1. Turn instance methods into stateless functional components

Our 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.

2. Extract remaining instance methods to plain functions

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.

3. Extract branching complexity with higher-order components

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.

4. Create a data fetching higher-order component

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!

5. Compose behaviors into a single enhance() function

Currying 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 diffs!

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().

Final result

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 enhancer 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.

It's a wrap, err, sandwich

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.

Jonathan Lee Martin

Jonathan is an educator, writer and international speaker. He guides developers — from career switchers to senior developers at Fortune 100 companies — through their journey into web development.