avatarEric Elliott

Summary

The article advocates for React developers to learn function composition to efficiently manage cross-cutting concerns in their applications, such as authentication, feature management, logging, and layout rendering, by using higher-order components (HOCs), function currying, and asynchronous function composition in API routes.

Abstract

The article "Why Every React Developer Should Learn Function Composition" emphasizes the importance of managing cross-cutting concerns in React applications through the use of advanced functional programming techniques. It begins by illustrating common repetitive tasks in React applications, such as user authentication checks and standard layout rendering, and proposes the use of provider components to reduce boilerplate code. The author then introduces higher-order components (HOCs) as a more effective solution for wrapping page components with additional functionalities. The concept of function composition is presented as a means to combine multiple HOCs into a single provider, enhancing code reusability and maintainability. The article further explores function currying as a method to create specialized HOCs and discusses the application of asynchronous function composition in API routes to handle concerns like authentication, authorization, validation, logging, and error handling. The author also provides practical examples and code snippets to demonstrate these concepts and suggests that mentorship can be a valuable asset for career development in software engineering.

Opinions

  • The author believes that copy-pasting boilerplate code into every component is an inefficient practice that leads to maintenance challenges.
  • Higher-order components (HOCs) are seen as a superior alternative to provider components for abstracting cross-cutting concerns.
  • Function composition is considered a powerful tool for building complex applications by combining simpler functions.
  • Currying functions is recommended for creating customizable HOCs that can cater to the specific needs of different pages or components.
  • The author promotes the use of function composition not only for client-side but also for server-side concerns in API routes, suggesting it as a replacement for traditional middleware stacks.
  • The article suggests that mentorship, rather than formal education, may have a stronger correlation with higher pay in the software development industry.
  • The author, Eric Elliott, advocates for the use of the techniques discussed in his book "Composing Software" and the mentorship services offered by DevAnywhere.io, implying that these resources can significantly benefit developers at all levels.

Why Every React Developer Should Learn Function Composition

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.

JavaScript
React
Technology
Software Development
Software Engineering
Recommended from ReadMedium