1.0.0-beta.2 • Published 5 years ago

react-hoc-pipe v1.0.0-beta.2

Weekly downloads
2
License
MIT
Repository
github
Last release
5 years ago

React HOC Pipe

Build Status Coverage Status npm version jest

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.

What are externalsParams ?

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" :question:

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(...)

:warning: externals params doesn't care about the call order. Externals parameters can be call before or after his HOC. Both 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>
    )
  }
}
1.0.0-beta.2

5 years ago

1.0.0-beta.1

5 years ago

0.1.2

6 years ago

0.1.1

6 years ago

0.1.0

6 years ago