React: ReactTestRenderer doesn't work with refs or ReactDOM.findDOMNode

Created on 29 Jul 2016  Â·  18Comments  Â·  Source: facebook/react

Jest snapshot testing uses ReactTestRenderer but if my component contains a ref or uses ReactDOM.findDOMNode it throws TypeError: component.getPublicInstance is not a function.

Component

import React from 'react';

export default class Link extends React.Component {
  render() {
    return (
      <a
        ref={a => this._a = a}
        href={this.props.page || '#'}>
        {this.props.children}
      </a>
    );
  }
}

Test

'use strict'

import React from 'react';
import Link from '../Link';
import renderer from 'react-test-renderer';

describe('Link', () => {
  it('renders correctly', () => {
    const tree = renderer.create(
      <Link page="foo" />
    ).toJSON();

    expect(tree).toMatchSnapshot();
  });
});

stack trace

 FAIL  __tests__/Link-test.js (2.148s)
● Link › it renders correctly
  - TypeError: component.getPublicInstance is not a function
        at attachRef (node_modules/react/lib/ReactRef.js:20:19)
        at Object.ReactRef.attachRefs (node_modules/react/lib/ReactRef.js:42:5)
        at attachRefs (node_modules/react/lib/ReactReconciler.js:26:12)
        at CallbackQueue._assign.notifyAll (node_modules/react/lib/CallbackQueue.js:67:22)
        at ReactTestReconcileTransaction.ON_DOM_READY_QUEUEING.close (node_modules/react/lib/ReactTestReconcileTransaction.js:37:26)
        at ReactTestReconcileTransaction.Mixin.closeAll (node_modules/react/lib/Transaction.js:204:25)
        at ReactTestReconcileTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:151:16)
        at batchedMountComponentIntoNode (node_modules/react/lib/ReactTestMount.js:61:27)
        at ReactDefaultBatchingStrategyTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:138:20)
        at Object.ReactDefaultBatchingStrategy.batchedUpdates (node_modules/react/lib/ReactDefaultBatchingStrategy.js:63:19)
DOM

Most helpful comment

This is not a bug. As explained above it is intentional.

The workaround is simple if you use jest. Just mock the third party component causing issues.

For example:

jest.mock('third-party-button', () => 'ThirdPartyButton');

Put this at the top of your test file.

Now any imports of third-party-button (replace this with your component) will become a string (e.g. ThirdPartyButton) so the component will become a “leaf” in the snapshot, like a div. Of course this won't actually test it, but it makes sense to only test your own code.

For libraries exporting multiple components, the workaround is similar but you'd want to provide a different mock. Something like:

jest.mock('third-party-lib', () => {
  return {
    ThirdPartyButton: 'ThirdPartyButton',
    OtherThing: 'OtherThing',
  };
}));

Finally, as last option, you can mock react-dom itself.

jest.mock('react-dom', () => ({
  findDOMNode: () => ({}),
});

Note you can return anything there and adjust it to match the expectations of the tested code.

All of this works if you use Jest. Unfortunately I don’t have good solutions for other runners.

All 18 comments

Thanks for the well documented issue! I'll leave it to @spicyj since he did the hard work on this.

Hm, after https://github.com/facebook/react/pull/7258 (which should be in 15.3.0) it give you null for any ref and should not throw. It's expected that ReactDOM.findDOMNode doesn't work.

So would it be safe to say that snapshot testing on something like react-frame-component wouldn't work?

Looks like React also logs a warning when using refs with react-test-renderer now:

Warning: Stateless function components cannot be given refs (See ref "main" in a component created by DummyComponent). Attempts to access this ref will fail.

caused by code similar to this example:

const DummyComponent = React.createClass({
    render() {
        return <div ref="main">content</div>;
    }
});

(I can put up an example on github if you want)

So would it be safe to say that snapshot testing on something like react-frame-component wouldn't work?

It wouldn’t, but there are a few possible options:

  • You can mock it out (assuming you use Jest)
  • In the future test renderer could provide a first-class component mocking API out of the box, unrelated to Jest
  • We could give you a mock object that looks like a DOM node but doesn’t actually do anything

Any solutions you prefer?

We had the same problem, but solved it using the ref callback as described (with examples) here.
https://facebook.github.io/react/docs/more-about-refs.html#the-ref-callback-attribute

componentWillMount: function() {
  this._refs = {};
},
componentDidMount: function() {
  this._refs[`textRefId`].focus();
},
render: function() {
  return <TextInput ref={(c) => this._refs[`textRefId`] = c; } />;
},

@gaearon sorry missed your comment.

We could give you a mock object that looks like a DOM node but doesn’t actually do anything

That would be the preference as I'm asserting the children of the frame component.

I don't think providing a mock DOM node would be the best solution here. We'd have to implement the full DOM node API so that when it's actually used it doesn't throw. It would be better if we could have an optional integration with jsdom (or whatever the user chooses to use), so that we can use a full implementation of the DOM and avoid having to implement and maintain our own mock API.

Refs work with test renderer in master, and you can even mock them to return something else instead of null:

import React from 'react';

export default class MyInput extends React.Component {
  componentDidMount() {
    this.input.focus();
  }
  render() {
    return (
      <input ref={node => this.input = node} />
    );
  }
}
import React from 'react';
import MyInput from './MyInput';
import renderer from 'react-test-renderer';

function createNodeMock(element) {
  if (element.type === 'input') {
    return {
      focus() {},
    };
  }
  return null;
}

it('renders correctly', () => {
  const tree = renderer.create(
    <MyInput />,
    {createNodeMock}
  ).toJSON();
  expect(tree).toMatchSnapshot();
});

There are no plans to support findDOMNode() in test renderer because it should be agnostic of React DOM and there is no way to implement it in a way that won't break in the future.

For some reason I still can't use ref in jest tests. Details here: http://stackoverflow.com/questions/40852131/cant-get-ref-in-jest-tests

@romanoff

React test renderer is not coupled to React DOM. It can't "guess" which DOM APIs your component relies on. You need to mock the nodes yourself, as noted in 15.4.0 release notes. I hope this helps!

What is the recommended heuristic to detect that findDOMNode won't work, in order to take a different code path? (try / catch could work but isn't very clear)

Edit: to elaborate: this is for a component library, so mocking (inside the library at least) is not an option

Is there any idea how to overcome this bug? I have few third party components (like https://github.com/ericgio/react-bootstrap-typeahead/) which uses ReactDOM.findDOMNode how could I use jest snapshots with them?

any help for the 10-thumbs-up comment will be really appreciated :D

This is not a bug. As explained above it is intentional.

The workaround is simple if you use jest. Just mock the third party component causing issues.

For example:

jest.mock('third-party-button', () => 'ThirdPartyButton');

Put this at the top of your test file.

Now any imports of third-party-button (replace this with your component) will become a string (e.g. ThirdPartyButton) so the component will become a “leaf” in the snapshot, like a div. Of course this won't actually test it, but it makes sense to only test your own code.

For libraries exporting multiple components, the workaround is similar but you'd want to provide a different mock. Something like:

jest.mock('third-party-lib', () => {
  return {
    ThirdPartyButton: 'ThirdPartyButton',
    OtherThing: 'OtherThing',
  };
}));

Finally, as last option, you can mock react-dom itself.

jest.mock('react-dom', () => ({
  findDOMNode: () => ({}),
});

Note you can return anything there and adjust it to match the expectations of the tested code.

All of this works if you use Jest. Unfortunately I don’t have good solutions for other runners.

Thanks a lot for this helpful comment 👍🏻
I'm sure your explanation will be helpful to many jest noobz like myself!

This is how I mocked 3rd party dependency that used react-dom findDOMNode in my app.

/// SomeComponent.test.js
jest.mock('some-library/lib/MockThis', () => {
  const Mock = require.requireActual('./MockForMockThis');
  return Mock;
});

// test....

Drawback in this is that if that 3rd party dependency changes diretory structure etc, then this will break.

Mocking refs and canvas was easy using:

jest.mock('react-dom', () => ({
    findDOMNode: () => ({
      getContext: jest.fn(),
    }),
  })
);
Was this page helpful?
0 / 5 - 0 ratings