Divide that HOC and HOConquer

Thursday, May 30, 2019

Intro

When creating react+redux web sites I usually create HOCs to connect with the store. These HOCs are usually pretty uncomfortable to be used: They need other HOCs or produce too much traffic or complicated flows. All situations can be avoided, but at that point the HOC becomes harder to be created.

Exhibit #1

This is what I mean:

type ExternalProps = RouteComponentProps<{id: string}>

export type Props = State & typeof actionCreators

export const withCurrentItem = <TOriginalProps extends {} = {}>(
	Inner: React.ComponentType<TOriginalProps & ExternalProps & Props>) => {
	type ResultProps = TOriginalProps & ExternalProps
	type WrapperProps = ResultProps & Props

	class Wrapper extends React.Component<WrapperProps> {
		public componentWillMount() {
			this.props.loadIfNeeded(this.props.match.params.id)
		}

		public render() {
			return (<Inner {...this.props} />)
		}
	}

	return connect<State, typeof actionCreators, ResultProps>(
		(state: ApplicationState) => state.currentItem,
		actionCreators
	)(Wrapper)
}

The problem with this HOC is that it does too many things. Let see:

  1. Connects to store providing state and actionCreators
  2. Asks actionCreators to load if needed
  3. Receive parameters from router, that you’d need to provide later by applying another HOC

Using this HOC might look something like:

const Page = withRouter(withCurrentItem(StatelessPage))

That might be kind of OK for the page but it will become cumbersome as we need to do it for many other smaller components around and perhaps at that point we don’t need neither the actions nor the loading, nor the router.

So let’s break it down.

Store: State + Actions

We will create a HOC that just connects to the store. Someone else must make sure the store loads current item. Components enriched with this HOC will have read access to current item and will be able to perform actions with it.

export type Props = State & typeof actionCreators

export const withCurrentItem = <TOriginalProps extends {} = {}>(
	Inner: React.ComponentType<TOriginalProps & Props>) => {
	type ResultProps = TOriginalProps
	type WrapperProps = ResultProps & Props

	class Wrapper extends React.Component<WrapperProps> {
		public render() {
			return (<Inner {...this.props} />)
		}
	}

	return connect<State, typeof actionCreators, ResultProps>(
		(state: ApplicationState) => state.currentItem,
		actionCreators
	)(Wrapper)
}

using it:

const NiceItemWidgetWithDeleteButton = withCurrentItem(StatelessNiceItemWidgetWithDeleteButton)

Autoload

This one will take care of loading current item, for instance on top level components.

export const autoLoadCurrentItem = <TOriginalProps extends {} = {}>(
	Inner: React.ComponentType<TOriginalProps>) => {
	
	type ResultProps = TOriginalProps
	type WrapperProps = ResultProps
		& typeof actionCreators
		& State
		& RouteComponentProps<{ id: ItemId }>

	class Wrapper extends React.Component<WrapperProps> {
		public componentDidMount() {
			const props = this.props
			props.loadIfNeeded(props.match.params.id)
		}
		public render() {
			const props = this.props
			return <Inner { ...props }/>
		}
	}
	
	const WrapperWithRouter = withRouter(Wrapper)
	return connect<State, typeof actionCreators, WrapperProps>(
		(state: ApplicationState) => state.currentItem,
		actionCreators
	)(WrapperWithRouter)
}

Using it:

const Page = autoLoadCurrentItem(InnerPage)

Outro

Single responsibility principle applies everywhere. By splitting you get a much more intuitive code base. It is also easier to build more complex logic into a HOC that does only one thing. Client code will also be simpler if you don’t need to worry about other things the HOC needs to work properly.