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:
- Connects to store providing
state
andactionCreators
- Asks
actionCreators
to load if needed - 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.