Why Every React Developer Should Learn Function Composition

Eric Elliott
JavaScript Scene
Published in
6 min readSep 1, 2022

--

Imagine you’re building a React application. There are a number of things you want to do on just about every page view of the application.

  • Check and update user authentication status
  • Check currently active features to decide which features to render (needed for continuous delivery)
  • Log each page component mount
  • Render a standard layout (navigation, sidebars, etc)

Things like this are commonly called cross-cutting concerns. At first, you don’t think of them like that. You just get sick of copy-pasting a bunch of boilerplate code into every component. Something like:

const MyPage = ({ user = {}, signIn, features = [], log }) => {
// Check and update user authentication status
useEffect(() => {
if (!user.isSignedIn) {
signIn();
}
}, [user]);
// Log each page component mount
useEffect(() => {
log({
type: 'page',
name: 'MyPage',
user: user.id,
});
}, []);
return <>
{
/* render the standard layout */
user.isSignedIn ?
<NavHeader>
<NavBar />
{
features.includes('new-nav-feature')
&& <NewNavFeature />
}
</NavHeader>
<div className="content">
{/* our actual page content... */}
</div>
<Footer /> :
<SignInComponent />
}
</>;
};

We can get rid of some of that cruft by abstracting all those things into separate provider components. Then our page could look something like this:

const MyPage = ({ user = {}, signIn, features = [], log }) => {
return (
<>
<AuthStatusProvider>
<FeatureProvider>
<LogProvider>
<StandardLayout>
<div className="content">{/* our actual page content... */}</div>
</StandardLayout>
</LogProvider>
</FeatureProvider>
</AuthStatusProvider>
</>
);
};

We still have some problems though. If our standard cross-cutting concerns ever change, we need to change them on every page, and keep all the pages in-sync. We also have to remember to add the providers to every page.

Higher Order Components

A better solution is to use a higher-order component (HOC) to wrap our page component. This is a function that takes a component and returns a new component. The new component will render the original component, but with some additional functionality. We can use this to wrap our page component with all the providers we need.

const MyPage = ({ user = {}, signIn, features = [], log }) => {
return <>{/* our actual page content... */}</>;
};
const MyPageWithProviders = withProviders(MyPage);

Let’s take a look at what our logger would look like as a HOC:

const withLogger = (WrappedComponent) => {
return function LoggingProvider ({ user, ...props }) {
useEffect(() => {
log({
type: 'page',
name: 'MyPage',
user: user.id,
});
}, []);
return <WrappedComponent {...props} />;
};
};

Function Composition

To get all our providers working together, we can use function composition to combine them into a single HOC. Function composition is the process of combining two or more functions to produce a new function. It’s a very powerful concept that can be used to build complex applications.

Function composition is the application of a function to the return value of another function. In algebra, it’s represented by the function composition operator: ∘

(f ∘ g)(x) = f(g(x))

In JavaScript, we can make a function called compose and use it to compose higher order components:

const compose = (...fns) => (x) => fns.reduceRight((y, f) => f(y), x);const withProviders = compose(
withUser,
withFeatures,
withLogger,
withLayout
);
export default withProviders;

Now you can import withProviders anywhere you need it. We're not done yet, though. Most applications have a lot of different pages, and different pages will sometimes have different needs. For example, we sometimes don't want to display a footer (e.g. on pages with infinite streams of content).

Function Currying

A curried function is a function which takes multiple arguments one at a time, by returning a series of functions which each take the next argument.

// Add two numbers, curried:
const add = (a) => (b) => a + b;
// Now we can specialize the function to add 1 to any number:
const increment = add(1);

This is a trivial example, but currying helps with function composition because a function can only return one value. If we want to customize the layout function to take extra parameters, the best solution is to curry it.

const withLayout = ({ showFooter = true }) =>
(WrappedComponent) => {
return function LayoutProvider ({ features, ...props}) {
return (
<>
<NavHeader>
<NavBar />
{
features.includes('new-nav-feature')
&& <NewNavFeature />
}
</NavHeader>
<div className="content">
<WrappedComponent features={features} {...props} />
</div>
{ showFooter && <Footer /> }
</>
);
};
};

But we can’t just curry the layout function. We need to curry the withProviders function as well:

const withProviders = (options) =>
compose(
withUser,
withFeatures,
withLogger,
withLayout(options)
);

Now we can use withProviders to wrap any page component with all the providers we need, and customize the layout for each page.

const MyPage = ({ user = {}, signIn, features = [], log }) => {
return <>{/* our actual page content... */}</>;
};
const MyPageWithProviders = withProviders({
showFooter: false
})(MyPage);

Function Composition in API Routes

Function composition isn’t just useful on the client-side. It can also be used to handle cross-cutting concerns in API routes. Some common concerns include:

  • Authentication
  • Authorization
  • Validation
  • Logging
  • Error handling

Like the HOC example above, we can use function composition to wrap our API route handler with all the providers we need.

Next.JS uses lightweight cloud functions for API routes, and no longer uses Express. Express was primarily useful for its middleware stack, and the app.use() function, which allowed us to easily add middleware to our API routes.

The app.use() function is just asynchronous function composition for API middleware. It worked like this:

app.use((request, response, next) => {
// do something
next();
});

But we can do the same thing with asyncPipe - a function that you can use to compose functions which return promises.

const asyncPipe = (...fns) => (x) =>
fns.reduce(async (y, f) => f(await y), x);

Now we can write our middleware and API routes like this:

const withAuth = async ({request, response}) => {
// do something
};

In the apps we build, we have function that creates server routes for us. It’s basically a thin wrapper around asyncPipe with some error handling built-in:

const createRoute = (...middleware) =>
async (request, response) => {
try {
await asyncPipe(...middleware)({
request,
response,
});
} catch (e) {
const requestId = response.locals.requestId;
const { url, method, headers } = request;
console.log({
time: new Date().toISOString(),
body: JSON.stringify(request.body),
query: JSON.stringify(request.query),
method,
headers: JSON.stringify(headers),
error: true,
url,
message: e.message,
stack: e.stack,
requestId,
});
response.status(500);
response.json({
error: 'Internal Server Error',
requestId,
});
}
};

In your API routes, you can import and use it like this:

import createRoute from 'lib/createRoute';
// A pre-composed pipeline of default middleware
import defaultMiddleware from 'lib/defaultMiddleware';

const helloWorld = async ({ request, response }) => {
request.status(200);
request.json({ message: 'Hello World' });
};

export default createRoute(
defaultMiddleware,
helloWorld
);

With these patterns in place, function composition forms the backbone that brings together all of the cross cutting concerns in the application.

Any time you find yourself thinking, “for every component/page/route, I need to do X, Y, and Z”, you should consider using function composition to solve the problem.

Next Steps

Composing Software is a best-selling book that covers composition topics in a lot more depth.

Did you know that mentorship correlates better with higher pay than a college degree? We founded DevAnywhere.io to provide mentorship to software builders at every level. From junior to CTO, no matter where you are in your journey, we have a mentor who can help you reach the next level. It may just be the best investment you ever make in your career.

Eric Elliott is a tech product and platform advisor, author of “Composing Software”, cofounder of EricElliottJS.com and DevAnywhere.io, and dev team mentor. He has contributed to software experiences for Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, and top recording artists including Usher, Frank Ocean, Metallica, and many more.

He enjoys a remote lifestyle with the most beautiful woman in the world.

--

--