React HOC Pipe
Chain, setup and reuse Higher-Order Components easily accross your React application.
Motivation
On a React project, you often use the same HOC, with sometimes the same arguments. pipe()
enable to create a pipe of HOC, and reuse it accross your application.
A predefined HOC pipe, named pipeRequest
, is also provided. You can create your own pipe, or use pipeRequest and extends it with your HOCs.
Do you want to see a concrete example now? See HOC pipe request or a full reusable HOC pipe
Install
npm install react-hoc-pipe --save
or with yarn
yarn add react-hoc-pipe
HOC Pipe
pipe()
is a reusable pipe of HOC.
How to create a pipe ?
const hocs = {
myFirstHOC: { ... }
mySecondHOC: { ... }
}
const myPipe = () => pipe(hocs)
Then, reuse it !
class Component extends React.Component {
...
}
const App = myPipe()
.myFirstHOC(params)
.mySecondHOC()
.render(Component)
...
render() {
return <App />
}
How to define HOC ?
const hocs = {
withData: {
externalsParams: [],
HOC: externalsParams => data => App => {
return class Request extends React.Component {
render() {
return <App {...this.props} {...data} />
}
}
},
},
}
-
externalsParams
- optional - functions to set parameters that will be used inside HOC. More detail -
data
- optional - yours HOC arguments -
App
- React Component
Full example:
const hocs = {
withData: {
externalsParams: [],
HOC: (externalsParams) => (data) => App => {
return class Request extends React.Component {
render() {
return <App {...this.props} {...data} />
}
}
},
}
}
const myPipe = () => pipe(hocs)
...
const App = myPipe()
.withData({ foo: 'bar' })
.render((props) => {
return <div>{props.foo}</dib>
})
I know, this example is completely useless. But it's simple. You can then build your complex HOC.
externalsParams
?
What are externalsParams
are functions to set parameters that will be used inside HOC. It's usefull because you can set paramters before or after the HOC call in the pipe.
const hocs = {
withData: {
externalsParams: ['addData']
HOC: (externalsParams) => (data) => App => {
return class Request extends React.Component {
render() {
// addData[0] correspond to the first argument of addData()
const externalsData = externalsParams.addData[0];
return <App {...this.props} {...data} {...externalsData} />
}
}
},
}
}
const myPipe = () => pipe(hocs)
...
const App = myPipe()
.withData({ foo: 'bar' })
.addData({ foo2: 'bar2' })
.render((props) => {
return <div>{props.foo} {props.foo2}</dib>
})
"Wait, why do not simply use only withData()
and pass all data through it"
Good question ! And the anwser is simple: sometimes, you want to reuse a pipe, with the same parameter 95% of the time, and 5% remaining, you want to override it.
Example:
const hocs = {
request: {
externalsParams: ['renderLoader'],
HOC: ...,
}
}
const Loader = () => <div>...</div>
const myPipe = () => pipe(hocs).renderLoader(Loader)
...
const Page1 = myPipe()
.request(...)
.render(...)
...
const Page2 = myPipe()
.request(...)
.render(...)
You defined your spinner only once, and it will be use into all of your myPipe()
, until you override it. If you want to override it for a specific component, it's simple:
const Page1 = myPipe()
.request(..)
.renderLoader(() => <div>Loading...</div>)
.render(...)
Page1
and Page2
are equivalent:
const Page1 = myPipe()
.request()
.renderLoader()
.render()
const Page2 = myPipe()
.renderLoader()
.request()
.render()
However, the call order of HOC is important !
Page1
and Page2
are not the same:
const Page1 = myPipe()
.connect(...)
.request(...)
.render(Component)
const Page2 = myPipe()
.request()
.connect()
.render(Component)
The classique HOC syntax correspond to this:
const Page1 = connect(...)(request(...)(Component))
const Page2 = request(...)(connect(...)(Component))
Real world examples
Do you want a full real usefull example ? Well. I made a HOC pipe focus on the request handling.
Pipe Request
pipeRequest()
is a predefined HOC pipe, focus on the request feature. It makes it possible to perform a request, show a loader, map request results to props, and then render your component,
import React from 'react'
import { pipeRequest } from 'react-hoc-pipe'
class MyComponent extends React.Component {
...
}
const App = pipeRequest()
.request((props) => fetch('http://website.com/posts'))
.mapRequestToProps((response) => ({ posts: response.posts }))
.renderLoader(() => <div>Loading...</div>)
.render(MyComponent)
Just below, the documentation of pipeRequest()
import { pipeRequest } from 'react-hoc-pipe'
/**
* hocs are optional
*/
const hocs = {
...
}
const App = pipeRequest(hocs)
/**
* async request. Must return a Promise
*
* optional
*/
.request(props => fetch('...'))
/**
* map the request results to props
*
* By default, request results are not sent as props to the render()
* Because sometime, you doesn't want to get the results directly.
* It's the case if you use redux actions with dispatch. You perform a request,
* but don't want to get the result from the Promise
*
* optional
*/
.mapRequestToProps(response => ({
foo: response.bar,
}))
/**
* Functionnal component or class component
*
* It's render during the request process
* If there is no renderLoader, final render is use
*
* optional
*/
.renderLoader(() => <div>is Loading...</div>)
/**
* Functionnal component or class component
* Final render
*
* required
*/
.render(Component)
HOC with arguments, like redux connect()
Here, I will use pipeRequest()
, but if you doesn't need request handling, you can use pipe()
import { pipeRequest } from 'react-hoc-pipe'
import { connect } from 'react-redux'
const hocs = {
connect: {
HOC: (externalsParams) => (mapStateToProps, mapDispatchToProps) => App => {
return connect(mapStateToProps, mapDispatchToProps)(App);
},
// Or the simpler and shorter version
HOC: (externalsParams) => connect
}
}
const App = pipeRequest({ connect })
.connect(
mapStateToProps,
mapDispatchToProps,
)
.request(...)
.mapRequestProps(...)
.renderLoader(...)
.render(props => <div>...</div>)
HOC without arguments, like withRouter()
If you use externals HOC without argument, like withRouter()
, the syntaxe is a bit different than HOC with arguments, like connect()
.
connect: connect(params)(App)
withRouter: withRouter(App)
As you can see, connect()
take params and return another function, while withRouter()
directly take the React component as parameter. So the externals HOC config is a bit different.
Note: in the following examples, externalsParams
are useless.
import { pipeRequest } from 'react-hoc-pipe'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
const hocs = {
connect: {
HOC: (externalsParams) => connect
}
withRouter: {
HOC: (externalsParams) => () => withRouter
}
}
const App = pipeRequest(hocs)
.connect(
mapStateToProps,
mapDispatchToProps,
)
.request(props => fetch('...'))
.withRouter()
.render(props => <div>...</div>)
Enhance redux connect() with externals params
externals params can be usefull for defined mapDispatchToProps
, if you often use the same actions.
import { pipeRequest } from 'react-hoc-pipe'
import { connect } from 'react-redux'
import { fetchUser } from 'src/store/actions'
const hocs = {
connect: {
externalsParams: ['mapDispatchToProps']
HOC: (externalsParams) => (mapStateToProps, mapDispatchToProps) => {
const finalMapDispatchToProps = externalsParams.mapDispatchToProps || mapDispatchToProps
return connect(mapStateToProps, finalMapDispatchToProps)
}
}
}
const myPipeRequest = () => pipeRequest(hocs).mapDispatchToProps({ fetchUser })
...
const App = myPipeRequest()
.connect(mapStateToProps)
.request(props => props.fetchUser()) // fetchUser is binded to redux store
.render(props => <div>...</div>)
Full reusable HOC pipe
import { pipeRequest } from 'react-hoc-pipe'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { fetchUser } from 'src/store/actions'
const hocs = {
connect: {
HOC: () => connect,
},
withRouter: {
HOC: () => () => withRouter,
},
}
const pipeWithUser = externalsHOCs =>
pipeRequest({ hocs, ...externalsHOCs })
.withRouter()
.connect(
(state, props) => {
const userId = props.match.params.userId
return {
userId,
user: state.users[userId],
}
},
{ fetchUser },
)
.request(props => props.fetchUser(userId))
.renderLoader(() => <div>Fetching user...</div>)
Then, reuse it !
const UserView = (props) => (
<div>
<div>{props.user.firstName}</div>
<div>{props.user.lastName}</div>
</div>
)
const User = pipeWithUser().render(UserView)
...
class Page extends React.Component {
render() {
return (
<div>
<User />
</div>
)
}
}