glenjamin/skin-deep

subTree functions can't find stateless functional components

wesleyfok opened this issue · 2 comments

Apologies if there's actually an easy answer for this (I hope so!) or if this should actually go to the React project directly. I'm still pretty new to React so it's quite possible I've messed up somewhere.

Consider the following React structure for a ProductInfoForm:

import React from 'react';

import LabelPrefixedInput from './LabelPrefixedInput';

export default class ProductInfoForm extends React.Component {
    render() {
        const {product} = this.props;
        if (!product) {
            return (
                <div>No product selected</div>
            )
        } else {
            return (
                <form>
                    <fieldset>
                        <ul>
                            <LabelPrefixedInput type="text" name="name" label="Name:" value={product.title} />
                            <LabelPrefixedInput type="text" name="sku" label="Model/SKU:" value={product.sku} />
                        </ul>
                    </fieldset>
                </form>
            )
        }
    }
};

and the following definition of a LabelPrefixedInput:

import React from 'react';

export default ({type, name, label, value, onChange}) => (
    <li>
        <label>{label}</label>
        <input type={type} name={name} value={value} onChange={onChange} />
    </li>
);

I'm using skin-deep with QUnit. If I try to run the following code in a test:

const componentTree = SkinDeep.shallowRender(<ProductInfoForm product={product} />);
assert.equal(componentTree.everySubTree('LabelPrefixedInput').length, 8, 'ProductInfoForm has eight text fields.');

I get a failure; componentTree.everySubTree('LabelPrefixedInput') finds no LabelPrefixedInput elements at all. However, if I redefine LabelPrefixedInput as a class instead of a function, like so:

import React from 'react';

export default class LabelPrefixedInput extends React.Component {
    render() {
        const {type, name, label, value, onChange} = this.props;
        return (
            <li>
                <label>{label}</label>
                <input type={type} name={name} value={value} onChange={onChange} />
            </li>
        );
    }
}

then everySubTree() finds all the LabelPrefixedInput instances as expected.

I think the issue is that getRenderOutput() gives a type of 'exports.default()' for functional stateless components instead of the class name it gives for components defined as classes. This makes a certain amount of sense, but from what I can tell it also means there's no way to find components defined as functions. It would also mean that we'd have to write all components as classes in order to use skin-deep with them.

Again, apologies if I've missed something obvious.

The problem here is that your component doesn't have a name - LabelPrefixedInput is just the name of the variable you're importing it with.

If you use the React DevTools you'll probably see something very similar.

Personally, I always define functional components using the function keyword, and name that function, like this:

export default function LabelPrefixedInput({type, name, label, value, onChange}) {
  return (
    <li>
        <label>{label}</label>
        <input type={type} name={name} value={value} onChange={onChange} />
    </li>
  );
}

Another way to give names to arrow functions is to assign them to a local variable, like so:

const LabelPrefixedInput = ({type, name, label, value, onChange}) => (
    <li>
        <label>{label}</label>
        <input type={type} name={name} value={value} onChange={onChange} />
    </li>
);
export default LabelPrefixedInput;

See http://www.2ality.com/2015/09/function-names-es6.html for more info.

Got it, so I WAS missing something. Thanks!