React Higher-Order Components (HOC) using TypeScript

React Higher-Order Components (HOC) using TypeScript

Higher-order components and monad have something in common, both sound esoteric, but at least higher-order component (HOC) is easy to explain. It is a component that returns a new component. The reason for doing so is to provide shared functionality to multiple components. The React documentation has a lot of content on caveats, conventions, and examples. In this blog post, I'll focus more on an implementation using TypeScript.

Overview

HOCs are used to address cross-cutting concerns which is a fancy term for shared functionality. Examples of cross-cutting concerns in software development are logging, security, data transfer, etc. Most of the HOCs come from 3rd party libraries. Many popular React libraries are using HOCs, for example, react-redux or i18next-react. Because many libraries already solve the issue at hand, custom HOCs, in my experience, are implemented quite rarely.

How does a usage of HOC look like? A simplified example from the i18next-react web-site:

import React from 'react';
import { translate } from 'react-i18next';

function TranslatableView(props) {
  const { t } = props;

  return (
    <div>
      <h1>{t('keyFromDefault')}</h1>
    </div>
  )
}

export default translate()(TranslatableView);

The last line is important. translate() is a call to a function that will create HOC. HOC will create a component which will add translation functionality to the TranslatableView.

Example use-case: adding ASP.NET Anti-Forgery Validation Token to the component

I had a scenario where I had to add a token as part of the POST request headers. The token, in this case, was a security token created by ASP.NET MVC called Anti-Forgery Validation Token.

After dispatching Redux actions that will do the call to the API endpoint, I noticed repetition in the source code. One of the often repeated code blocks was getting the token and placing it the action payload. I didn't want to use a singleton or global state to provide the token as it would make the code harder to test. Instead, I took it from the Redux store and explicitly added to the requests that needed it.

All components that dispatched an async action had to have following code:

const mapStateToProps = (
  state: { settings: AppSettings },
  props: DeleteExampleProps 
): DeleteExampleProps => ({
  ...props,
  token: state.settings.token
})

const mapDispatchToProps = (
  dispatch,
  props: DeleteExampleProps 
): DeleteExampleProps => ({
  ...props,
  delete: (id: number) => {
    dispatch(deleteWorker({ id, token: props.token }))
  }
})

export default connect<DeleteExampleProps, DeleteExampleProps, any>(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent)

The idea of this snippet is to get the token from the store state (mapStateProps) and when dispatching (mapDispatchToProps) an action add it to the payload.

What I did to reduce repetition was an HOC that I can use for components that are dispatching actions causing a POST request, for example, creating an entity.

Embedded JavaScript

Let's have a look at the AntiForgeryTokenized component signature.

export function AntiForgeryTokenized<P extends WithToken>(
  Comp: React.ComponentClass<P> | React.StatelessComponent<P>
): React.ComponentClass<P>

I can make sure that wrapped component props contain token by forcing its props to implement WithToken interface. Comp parameter is the component that will receive the shared functionality. As I am not using component's state, the Comp can be either ComponentClass or StatelessComponent.

Using the HOC is easy. Instead of using directly a component called MyComponent, I use the wrapped version.

import MyComponent from './MyComponent'
import AntiForgeryTokenized from '../HOC/AntiForgeryTokenized'

const TokenizedMyComponent = AntiForgeryTokenized(MyComponent)

...

render() {
  return <TokenizedComponent />
}

MyComponent needs to implement WithToken interface to be compliant with the defined HOC. This will ensure type-safety of the token and makes it explicit that token is required in this context.

export interface CommentAreaProps extends WithToken {
}

When I am dispatching an event, I am sure that the token is in props.

  delete: (id: number) => {
    dispatch(deleteWorker({ id, token: props.token }))
  }

Conclusion

I highly recommend reading the React documentation's chapter on higher-order functions as several caveats might not be obvious, for example, a section called "Don't Use HOCs Inside the render Method" describes one of them. After reading the chapter, I made some changes to my code also.

Even though I enjoy using TypeScript, HOCs type definitions sometimes make by head hurt as the usage of the type system is quite advanced.

If you have any questions don't hesitate to ask, for example, via Twitter.