Many people use React without embracing functional programming concepts and patterns. While this is fine, I've found it incredibly useful to adopt functional thinking when using React, since it is declarative and describes the user interface as a function of state. In so doing it empowers a fundamentally different way of creating GUI applications that isn't always obvious to beginners. In this article, I share how embracing functional thinking has been of benefit to me while building React applications.
I started my development career using software like Delphi, .NET WinForms and WPF to build desktop applications. My introduction to building rich browser applications came by way of jQuery and, when things got complicated, I used Backbone. All of these frameworks:
- Were object-oriented: Which meant that there could be two sources of truth: the data and the UI. I had to imperatively interact with objects when updating the UI, and then read the values back from the objects when a user interacted with it.
- Used data binding: This meant that, if the data changed, the UI would change automatically - and vice versa. The result was complicated dependency chains and hard-to-reason-about bugs where dependencies conflicted. When building a rich UI that had interdependencies between different pieces of data, it also often lead to performance issues.
These patterns were in stark contrast to the functional mindset, since mutating objects is frowned upon and often impossible. Additionally, behaviour is not attached to data in functional programming, as it is with objects, instead it is passed to functions and then new data is returned.
I then left desktop application development and moved into web development using Rails. In this new space, the web request lifecycle forced me to rebuild my UI for each request - ignoring caching. Since there was no two-way binding between UI and long-lived data to consider, data dependencies were simpler than desktop UI development. This changed the way I thought about building UIs.
Later I found myself back on the front-end and not enjoying the old patterns of data binding and imperative UIs. When working on Reminderapp.io, my team was forced to evaluate different frameworks due to the complex interaction model of the application. We needed to keep the UI in sync across multiple browsers while processing inputs, like SMS responses, from our users and their clients. When we tried React and embraced treating UI as a pure function of state - where data goes in, and UI comes out - we were able to easily reason about the data dependencies.
During my time as part of the Platform Engineering team at Allan Gray, I once again found happiness in React. This time the framework's simplicity and how it makes few assumptions about your application was cricital.
It does one thing well while remaining easy to compose with other tools.
Outside of work, I had been playing with Haskell which is a hard-knocks, steep learning curve way to learn functional programming. It helped ingrain several functional concepts in me, but two still stand out today: purity and composition. Being familiar with these concepts made them easier to spot in the React ecosystem, like React, Redux and even data manipulation in JSX. Embracing functional patterns slowly but completely changed the way I thought about building UIs and software in general.
React balances functional theory with practicality. You won't see advanced type theory in its documentation, but my experience has taught me that React is heavily influenced by functional programming concepts and especially functional reactive programming. While I don't think functional programming experience is a requirement for using React, I truly believe that you'll have a better time of it if you embrace functional concepts. Below, I delve into how two main functional concepts help me when using React.
Elements of Functional Programming in React
A pure function will always return the same value when called with the same arguments. It has no side-effects and its outputs are based solely on its inputs. I find that pure functions are useful because they are:
- Easy to reason about: Combining functions that have side-effects is an exercise in frustration, as you need to be aware of the hidden effects of each function and how they could interact with each other. With pure functions, this is not an issue since these are usually small functions that never have side-effects.
- Easy to test: When a function's outputs are solely dependant on it's inputs, testing it is a breeze. You don't need to worry about it carrying state around with it, you just pass it a range of inputs and make sure that the outputs are correct.
- Easy to compose: Pure functions can be chained, wrapped, and sequenced without concern. This is the most valuable aspect because it allows you to reuse the small, easy-to-reason-about, well-tested components in different ways.
In both the React and the Redux documentation, you are encouraged to write your application in pure functions as far as possible. They recommend that you first build what your UI should look like statically, and then add in state and other side-effects where necessary. Thinking like this helps to isolate side-effects, like state management and web requests, in what's become known as container components.
Writing as much of my React application as possible in pure components has meant that most of the application is easy to reason about, easy to test, and easy to reuse. Whereas, with object-oriented thinking, a UI would be a represented by a set of interdependent objects which might make testing complicated.
A functional application is simpler by its very nature.
Building a complete UI out of React components involves composing them together. A big realisation for me was that combining one or more React components still resulted in one React component. For instance, I can combine a Header component and a Body component to create an Article component:
const Article = () => <Header><Body /></Header>
Being able to break up your application into small parts, and then combine them together for different use cases, is powerful. This is because it allows for higher levels of re-use, while still being easy to reason about. For example, if I need to display a list of article summaries, I could compose the Header component with a ShortBody component to create a Summary component:
const Summary = () => <Header><ShortBody /></Header>
Then I could even compose several Summary components together to create a SummaryList component:
const SummaryList = () => [<Summary />, <Summary />, <Summary />]
In fact, I might not even need to create a new ShortBody component because I could truncate the text before I pass it to the Body component.
When every piece is small, there are many ways that you can choose to compose them.
It doesn't stop at components though! Redux is all about composing small functions together to make something bigger, and then composing that with other components to make something useful. Reducers are a great example of composing small functions together to create a more powerful function. When each reducer is small, you can put them together to get a function that effectively describes how your entire application's state works.
React applications are just a composition of various different side-effects with predominantly pure presentational components. Once I recognised this, I was able to more easily understand other React libraries, like i18n translations or web request management. Most of these libraries enrich your code by using higher-order components to compose side-effects with your pure components. Knowing this is helpful if you want to start adding your own side-effects, or building your own supporting libraries.
Building on what react teaches you about functional programming
I highly encourage you to explore functional programming if you haven't already. Once you're comfortable with things like higher-order functions, specifically data transformations like map and reduce, try to grapple with more advanced concepts like currying, partially applied functions, monoids and functors.
I can't overstate the excitement I felt when I first identified a monoid in an application that I was writing - yes, React components can form a monoid! Recognising this meant that I could reuse the patterns associated with it and also reason about it more effectively. I think it will be worthwhile - even if you just dip your toe into the lake that is functional programming concepts and patterns by learning about monoids!
Resources for further learning
- If you're interested in identifying pure and side-effect components, I gave a talk about identifying different categories of React components at ReactNext in Israel earlier this year.
- There has been some discussion around a functional approach to implementing hooks but, having read up on it and being a functional thinker myself, I think it would be the wrong approach for React to take for reasons of practicality. If you are interested in this, I recommend that you read some of Paul Gray's writings. In the end, there's nothing stopping anyone from taking a similar approach when introducing side-effects into their applications.
- Here are some articles to introduce you to monoids and functors.
- If you are interested in pursuing functional programming, and Haskell in particular, I highly recommend grabbing a copy of The Haskell Book. I must warn you though, once you start seeing code through the lens of functional programming, there's no going back! You'll forever be trying to avoid mentioning monads in mixed conversation at the risk of frightening off your colleagues.
Robert is a lover of all things code-related and passionate about sharing the "Aha!" moments he experiences. He has walked a winding path through the software world, but has most recently been involved with enterprise front-end applications and working on large applications with various sorts of teams. He enjoys talking at meetups and conferences as well as helping people level-up in whatever way he can.