eslint-plugin-react: Rule Proposal: functions returning JSX should be refactored into React components
Hey @yannickcr thanks for being awesome and making this great plugin, super valuable 😎.
I have noticed, that while working with a team of React devs - some devs will write functions that return JSX, but aren’t components. This usually makes things difficult to refactor later on, as we need to split out all of these “jsx fragment functions” into real React components.
For example, we often see things like this slip into our codebase:
export default class ArticleList extends React.Component {
renderImage(src, alt, caption) {
return (
<figure>
<img src={src} />
{ caption ? <figcaption>{caption}</figcaption> : null }
</figure>
);
}
renderArticle(articles) {
return articles.map((article) => (
<a href={item.url}>{this.renderImage(item.src, item.alt, item.caption)}</a>
));
}
render() {
return (
<div class="article-list">
{this.renderItems(this.props.articles)}
</div>
);
}
}
The problem with the above code, other than from a purity standpoint, is that it becomes a bit of a rats nest at any level of complexity, plus it is difficult to refactor when you need to. Ideally this code would be written as such:
function ArticleImage({ src, alt, caption }) {
return (
<figure>
<img src={src} />
{ caption ? <figcaption>{caption}</figcaption> : null }
</figure>
);
}
function ArticleItem({ url, src, alt, caption }) {
return (
<a href={url}>
<ArticleImage src={src} alt={alt} caption={caption}/>
</a>
);
}
export default function ArticleList({ articles }) {
return (
<div class="article-list">
{articles.map((article) => <ArticleItem {...article} />)}
</div>
);
}
The above code is much easier to reason about - each function that returns JSX has a well defined function signature (that of a react component) and each piece can be easily pulled out into its own component if the time comes.
Proposal
All of the above is just to demonstrate the proposal for the following rule:
react/no-jsx-outside-component: If a function returns JSX, it should be a React component (class or function).
The rule would check all functions in a file:
- if the function returned JSX
- or if the function returned React.createElement calls
- or if the function returned map/reduce calls which themselves return JSX
- the function is not a React.Component#render method
- or the function does not follow the React stateless function signature (
fn(props = {}))
Then the rule would warn that the function should be a React (stateless or class) component.
Let me know what your thoughts are on this. If you’re happy with the above proposal I’m happy to put the work into a PR for this.
About this issue
- Original URL
- State: open
- Created 8 years ago
- Reactions: 73
- Comments: 17 (6 by maintainers)
I’d argue that’s not breaking your tests, it’s improving them - by forcing you to split your tests up so that the new component has its own tests.
In other words, shallow rendering means each component primarily has tests that test what it renders, and they delegate testing responsibility for those things to their own tests.
Two problems:
const addProp = Component => props => <Component prop={‘a’} {…props} />
This is not a component but a component wrapper. I think in general, when you do a lot of functional manipulation with components, you will see quite a bunch of functions that have the signature of a component but are not meant as a component.
@eballeste I haven’t used react in a while but if I recall, we ultimately resovled to only ever use functional stateless compoments for everything except for components needing the low level life cycle hooks which would naturally get much closer scruitiny. If I was maintaining any react codebases anymore that’s the direction I’d certainly move in.
Now your test is aware of the implementation details of your component. 🙁 Decide that helper component isn’t needed, or split it into two, and your tests break again.
BTW, enzyme is great, and I love using it. 😉
@dirkroorda the wrapper
addPropabove is a function that returns an SFC - an SFC factory.@ljharb I thought that a component is a function with the signature ({ props }) => R or a class with a render function of that signature, where R is any piece of JSX.
The wrapper
addPropabove does not have that signature, so it is not a component. On the other hand, now I think of it, its result is not JSX, but a function that delivers JSX, so it is not a good example.