Render HOC(Component) without changing Component Name in JSX

Viewed 864

I have two HOCs that add context to a component like so :

const withContextOne = Component => class extends React.Component {
  render() {
    return (
      <ContextOne.Consumer>
        {context => <Component {...this.props} one={context} /> }
      </ContextOne.Consumer>
    );
  }
};
export default withContextOne;

Desired Result

I just want an syntactically concise way to wrap a component with this HOC so that it doesn't impact my JSX structure too much.

What I have tried

  1. Exporting a component with the HOC attached export default withContextOne(withContextTwo(MyComponent)) This way is the most concise, but unfortunately it breaks my unit tests.
  2. Trying to evaluate the HOC from within JSX like :

    { withContextOne(withContextTwo(<Component />)) } This throws me an error saying

Functions are not valid as a React child. This may happen if you return a Component instead of < Component /> from render.

  1. Creating a variable to store the HOC component in before rendering :

    const HOC = withContextOne(Component)

Then simply rendering with <HOC {...props}/> etc. I don't like this method as it changes the name of the component within my JSX

2 Answers

You can set the displayName before returning the wrapped component.

const withContextOne = Component => {
  class WithContextOneHOC extends React.Component {
    render() {
      return (
        <ContextOne.Consumer>
          {context => <Component {...this.props} one={context} /> }
        </ContextOne.Consumer>
      );
    }
  }

  WithContextOneHOC.displayName = `WithContextOneHOC(${Component.displayName})`;

  return WithContextOneHOC;
};

This will put <WithContextOneHOC(YourComponentHere)> in your React tree instead of just the generic React <Component> element.

You can use decorators to ease the syntactic pain of chained HOCs. I forget which specific babel plugin you need, it might (still) be babel-plugin-transform-decorators-legacy or could be babel-plugin-transform-decorators, depending on your version of babel.

For example:

import React, { Component } from 'react';
import { withRouter } from 'react-router';
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { resizeOnScroll } from './Resize';

@withRouter
@resizeOnScroll
@injectIntl
@connect(s => s, (dispatch) => ({ dispatch })) 
export default class FooBar extends Component {
  handleOnClick = () => {
    this.props.dispatch({ type: 'LOGIN' }).then(() => {
      this.props.history.push('/login');
    });
  }

  render() {
    return <button onClick={}>
      {this.props.formatMessage({ id: 'some-translation' })}
    </button>
  }
}

However, the caveat with decorators is that testing becomes a pain. You can't use decorators with const, so if you want to export a "clean" undecorated class you're out of luck. This is what I usually do now, purely for the sake of testing:

import React, { Component } from 'react';
import { withRouter } from 'react-router';
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { resizeOnScroll } from './Resize';

export class FooBarUndecorated extends Component {
  handleOnClick = () => {
    this.props.dispatch({ type: 'LOGIN' }).then(() => {
      this.props.history.push('/login');
    });
  }

  render() {
    return <button onClick={}>
      {this.props.formatMessage({ id: 'some-translation' })}
    </button>
  }
}

export default withRouter(
  resizeOnScroll(
    injectIntl(
      connect(s => s, ({ dispatch }) => ({ dispatch }))(
        FooBarUndecorated
      )
    )
  )
);

// somewhere in my app
import FooBar from './FooBar';

// in a test so I don't have to use .dive().dive().dive().dive()
import { FooBarUndecorated } from 'src/components/FooBar';
Related