Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[React] Add guidelines for setting displayName on higher-order components #968

Closed
lencioni opened this issue Jul 26, 2016 · 5 comments
Closed

Comments

@lencioni
Copy link
Contributor

Higher-order components are functions that take a Component as an argument and return a new Component that renders the passed-in Component. When doing this, it is nice to set the displayName on the generated Component to make it obvious (e.g. in dev tools) to understand what is going on. I prefer the convention of higherOrderComponent(WrappedComponent).

For example, if I have a higher-order component called withFoo and a component that is wrapped by this called Bar:

export default withFoo(Bar);

the displayName on the generated component would be set to withFoo(Bar), e.g.:

export default function withFoo(Component) {
  function WithFoo(props) {
    // TODO: do something special here.
    return <Component {...this.props} />;
  }

  WithFoo.displayName = `withFoo(${Component.displayName || Component.name}`;
  return WithFoo;
}
@ljharb
Copy link
Collaborator

ljharb commented Jul 26, 2016

This seems like a reasonable thing to add to the React guide (although imo HOCs should generally be avoided in favor of composition, when possible)

@lencioni
Copy link
Contributor Author

Agreed!

lencioni added a commit that referenced this issue Jul 26, 2016
This guideline will help us keep our higher-order components consistent
and easy to understand.

Fixes #968.
@jeffcarbs
Copy link

jeffcarbs commented Oct 21, 2016

@lencioni - I had a question about this pattern when using https://github.com/airbnb/enzyme (I'm assuming you use enzyme because you're an airbnb member).

Normally you can pass the component's name as a string as a selector. Imagine this scenario:

const ChildComponent = () => <div />
const ParentComponent = () => <ChildComponent />

describe('ParentComponent', () => {
  it('renders ChildComponent (select by class)', function() {
    const wrapper = shallow(<ParentComponent />)

    expect(wrapper.is(ChildComponent)).to.be.true
  })

  it('renders ChildComponent (select by class name)', function() {
    const wrapper = shallow(<ParentComponent />)

    expect(wrapper.is('ChildComponent')).to.be.true
  })
})

This all works fine.

Now imagine ChildComponent needs some added functionality, you decide to decorate it:

function withFoo(WrappedComponent) {
  function WithFoo(props) {
    // TODO: do something special here.
    return <WrappedComponent {...props} />
  }

  WithFoo.displayName = `withFoo(${WrappedComponent.displayName || WrappedComponent.name})`;
  return WithFoo;
}

@withFoo
class ChildComponent extends React.Component {
  render () {
    <div />
  }
}

const ParentComponent = () => <ChildComponent />

describe('ParentComponent', () => {
  it('renders ChildComponent (select by class)', function() {
    const wrapper = shallow(<ParentComponent />)

    expect(wrapper.is(ChildComponent)).to.be.true
  })

  it('renders ChildComponent (select by class name)', function() {
    const wrapper = shallow(<ParentComponent />)

    expect(wrapper.is('ChildComponent')).to.be.true
  })
})

The second test now fails!

It would need to be:

  it('renders ChildComponent (select by class name)', function() {
    const wrapper = shallow(<ParentComponent />)

    expect(wrapper.is('withFoo(ChildComponent)')).to.be.true
  })

I've been prefixing as this rule describes and in these cases I'll just import the component class and select by that expect(wrapper.is(ChildComponent)).to.be.true. However, this has always felt a little weird:

  • Sometimes ChildComponent is not something that should be exported, it's private to ParentComponent. So I'd have to add an export specifically for this test.
  • It forces you to import an additional component into the test file other than the component I'm directly testing.
  • import ChildComponent from './ChildComponent'; ChildComponent.displayName !== 'ChildComponent' 😐

Nothing earth shattering, but something akin to code smells so I'm wondering how other people handle it.

@lencioni
Copy link
Contributor Author

Related: airbnb/react-with-styles#32 (comment)

It is best to use wrapper.find() with a reference to the component instead of a string that describes the component (e.g. wrapper.find(MyComponent), not wrapper.find("MyComponent")). This will solve this problem for you when using any HOC, while also avoiding accidentally finding a different component than the one you expected that may have the same name.

For testing the underlying component with Enzyme, you probably want to use .dive().

I hope that helps! Let me know if you have any questions.

@jeffcarbs
Copy link

Cool, thanks for the response!

hibearpanda pushed a commit to 15Prospects/javascript that referenced this issue Jan 22, 2017
This guideline will help us keep our higher-order components consistent
and easy to understand.

Fixes airbnb#968.
jaylaw81 pushed a commit to appirio-digital/ads-best-practices that referenced this issue Sep 19, 2017
This guideline will help us keep our higher-order components consistent
and easy to understand.

Fixes airbnb#968.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants