Hi Nicholas,
I saw you tweet about JSX yesterday. It seemed like the discussion devolved pretty quickly but I wanted to share our experience over the last year. I understand your concerns. I've made similar remarks about JSX. When we started using it Planning Center, I led the charge to write React without it. I don't imagine I'd have much to say that you haven't considered but, if it's helpful, here's a pattern that changed my opinion:
The idea that "React is the V in MVC" is disingenuous. It's a good pitch but, for many of us, it feels like in invitation to repeat our history of coupled views. In practice, React is the V and the C. Dan Abramov describes the division as Smart and Dumb Components. At our office, we call them stateless and container components (view-controllers if we're Flux). The idea is pretty simple: components can't be concerned with both presentation and data-fetching. I feel like an example might be clearer...
A component like this would be rejected in code review for having both a presentation and data concern:
// CommentList.js
import React from "react";
class CommentList extends React.Component {
constructor() {
super();
this.state = { comments: [] }
}
componentDidMount() {
fetch("/my-comments.json")
.then(res => res.json())
.then(comments => this.setState({ comments }))
}
render() {
return (
<ul>
{this.state.comments.map(({ body, author }) =>
<li>{body}-{author}</li>
)}
</ul>
);
}
}
It would then be split into two components. The first is like a traditional template, concerned only with presentation, and the second is tasked with fetching data and rendering the related view component.
// CommentList.js
import React from "react";
const Commentlist = comments => (
<ul>
{comments.map(({ body, author }) =>
<li>{body}-{author}</li>
)}
</ul>
)
// CommentListContainer.js
import React from "react";
import CommentList from "./CommentList";
class CommentListContainer extends React.Component {
constructor() {
super();
this.state = { comments: [] }
}
componentDidMount() {
fetch("/my-comments.json")
.then(res => res.json())
.then(comments => this.setState({ comments }))
}
render() {
return <CommentList comments={this.state.comments} />;
}
}
In the updated example, CommentListContainer could shed JSX pretty simply.
render() {
return React.createElement(CommentList, { comments: this.state.comments });
}
Additionally, a Higher-order Component or component with Render Props could help in making container components and stateless components more composeable
When we started doing this, concerns about JSX vanished. Writing "dumb components" feels just the same as Handlebars or ERB templates but with the full power of JavaScript. We realized that it wasn't JSX that bothered us as much as the nagging feeling that components were just smaller balls of mud. For a while, our components were just smaller balls of mud but this pattern helped break the cycle.
I hope that this was a helpful addition to the conversation. I've written about it in slightly more detail here and here. You can also see Jason Bota's talk about how they do this at Facebook.
Cheers!
Michael
Quite helpful, thank you.
BTW, it seems to me in your
CommentList
component that you can do away entirely with theconstructor
as it is only callingsuper(props)
— remove the subclassedconstructor
and the prototypalconstructor
for React components will be called directly. Also, I think you missed athis
in your reference torenderComment
. It's not in scope, it's a prototypal method and needs to come from somewhere:Of course you may elect to keep the over-ridden
constructor
around out of habit / so that you can more easily add things to it in the future without having to type it up, but IMHO if the code is entirely redundant then why keep it around. Add it back in later if necessary.