Composing Higher-Order React Components in TypeScript

Higher-order components are a powerful tool for encapsulating behaviors, but using them isn’t always pretty:

const Foo: React.SFC<State & OwnProps> = (props) => {
  // ...
}

export default loadable(isLoading)(withUser(connect(mapStateToProps)(Foo)));

Since higher-order components are first-class objects, however, we can use a small compose utility to halt the rightward drift.

const FinalComponent = compose<OwnProps>(
  MyComponent,
  loadable(isLoading),
  connect(mapStateToProps),
  withUser,
)

All we need is the implementation.

import * as React from 'react'

type RC<P> = React.SFC<P> | React.ComponentClass<P>

type HOC<O, P> = (C: RC<O>) => RC<P>

// compose components from left to right
const compose = <P>(C: RC<P>, ...hocs: HOC<any, any>[]): RC<P> =>
  hocs.reduce((g, f) => f(g), C)

There’s quite a bit of hand-waving here. While the final component must have props matching <P>, the composed components can have any–a convenient if less-than-satisfying compromise. We don’t get type-safety within the composed component, but we do keep a variadic signature familiar from (e.g.) lodash.flow and interoperability with untyped 3rd-party components.

Someone out there has a clever way to address types of subsequent components. For now, validation of the “public” prop-types is better than nothing!




Reducing Typescript and Redux Boilerplate with Partial Actions

Static types can help reduce defects in evolving codebases, but their benefits need to balance out against the inevitable boilerplate. We’ve previously used TypeScript to secure the Redux dispatch flow, but the repetition involved didn’t feel good at the time and hasn’t gotten any better with age.

The problem is that we want to use type-narrowing to reveal logical issues, while still being able to describe like actions in generic terms. Consider a family of actions representing the states of an API request:

// actions.ts
type ListRequest = {
  count?: number
  page?: number
}

type Books = {
  resource: string,
  items: Array<Book>
}

export type Action =
  { type: 'LIST_BOOKS_REQUEST', request: ListRequest }
| { type: 'LIST_BOOKS_RESPONSE', request: ListRequest, response: Books }
| { type: 'LIST_BOOKS_ERROR', request: ListRequest, error: string }

We should be able to count on strong, static guarantees anywhere we dispatch or consume the LIST_BOOKS actions.

// reducers/books.ts
import { Action } from '../actions'

type Books = {
  isLoading: boolean,
  error: null | string,
  books: Array<Book>,
}

const initialState = {
  books: [],
  isLoading: false,
  error: null,
}

function books (state: Books = initialState, action: Action): Books {
  switch (action.type) {
    case 'LIST_BOOKS_REQUEST':
      return { ...state, isLoading: true }
    case 'LIST_BOOKS_ERROR':
      return { ...state, isLoading: false, error: action.error }
    case 'LIST_BOOKS_RESPONSE':
      return {
        error: null,
        isLoading: false,
        books: state.books.concat(action.response.items)
      }
    default:
      return state
  }
}

We would see a compiler error if books tried to access an unknown field. Good. But in order to create actions from the LIST_BOOKS family, we need to explicitly set both their types and their contents. In the previous attempt, we needed a fairly hefty definition to be able to represent asynchronous actions in a reasonably generic way:

const _listBooks: ApiAction<ListRequest, Books> = {
  request: (request) =>
    ({ type: 'LIST_BOOKS_REQUEST', request }),
  success: (response, request) =>
    ({ type: 'LIST_BOOKS_RESPONSE', request, response }),
  error: (error, request) =>
    ({ type: 'LIST_BOOKS_ERROR', request, error }),
}

function createApiAction<Q, S>(a: ApiAction<Q, S>, fetch: (q: Q) => Promise<S>) {
  return (req?: Q) => {
    // Make API request and handle result, dispatching
    // appropriate actions along the way
  }
}

export const listBooks = createApiAction(_listBooks, opts => api.books.list(opts))

The ApiAction type enables a generic interface, but the type conversion underpinning it in _listBooks is a compromise. Since we can’t define type strings dynamically, every new API action family we define will need its own implementation. The common interface is better than working with bespoke actions. But it’s hardly a joy.

Partial Actions

The React ecosystem has trained us (quite rightly) to prize obviousness. Using discrete actions to represent changes within the application makes it clear–sometimes painfully clear–to follow what’s going on. We’re about to break all that.

The little bargain we’re about to strike will but save us a heap of boilerplate, but at the cost of discreet actions. We’ll lose a degree of obviousness in reducer logic and middleware. We’ll keep type-safety. Goethe can opine as he will.

Ready?

Returning to our first example, let’s replace the three actions in the LIST_BOOKS family with a single type full of optional properties:

export type Action = {
  type: 'LIST_BOOKS',
  request?: ListRequest,
  response?: Books,
  error?: string,
}

We can think of this as a “partial” action. Like other forms of deferred state, the LIST_BOOKS action will be filled out with additional content as the request proceeds.

Remember that uncomfortable thing we were about to do? This is it. But using this pattern, we can now extract a generic type for the possible values of an API (or really any asynchronous) request:

type ApiAction<Q, S> = {
  request?: Q,
  response?: S,
  error?: string,
}

export type Action =
  ({ type: 'LIST_BOOKS' } & APIAction<ListRequest, Books>)

References to LIST_BOOKS elsewhere will still be narrowed to the correct type, but we can no longer refer to _ERROR and _SUCCESS actions representing the possible outcomes. Instead, we’ll need to derive state from the action’s content.

function books (prevState: Books = initialState, action: Action): Books {
  if (action.type === 'LIST_BOOKS') {
    const state = Object.assign({}, initialState)
    if (action.response) {
      state.books = state.books.concat(action.response.items)
    } else if (action.error) {
      state.error = action.error
    } else {
      state.isLoading = true
    }
    return state
  }
  return prevState
}

Conclusion

The upside? Reusing the type string across multiple states can save quite a bit of ceremony–in this example replacing the actions in a traditional TypeScript/redux implementation, boilerplate dropped by over 50%.

But whether it’s worth breaking the boundary between actions’ responsibilities will depend on the project and team. Partial actions are just a compromise between language and convention that may make sense in one context but not in another.

And as ever, let me know how it goes!




Books Worth Reading

Dry, stagnant, viscous words. When was the last time you walked away from Netflix long enough to read? To really read?

But where print falls short on candy-coated convenience, it fights its way back with ideas. Reading leaves time to pause, reread, and reflect on what’s just been said–a significant (if underrated) advantage over broadcast media. Want to think? First, read.

Reviews are one way to channel the effort. Knowing there’s a test (even a self-imposed one) at the end of the book encourages focus. Writing a review invites reflection on pivotal scenes and the story’s themes. And it yields a ready-packed answer to “should I read it?” and, “why?”

Treating reviews as recommendations makes it easier to separate out the “skims” from the “studies,” and to walk away from mediocre stories mid-flight. Save time for the books worth reading: packed with ideas, dripping with detail; rich stories filled with vivid imagery. Reviews help clarify the distinction.

If a story’s worth finishing, it’s worth taking a moment to mull over–and share–its big ideas. Down goes the book. Out comes the pen. And let me know how it goes!

View all posts