I know about Container Components. They usually go like that:
class ListContainer extends React.Component {
componentDidMount() {
fetch('/api/data')
.then(b => b.json())
.then(b => this.setState({data: b}));
}
render() { return <List data={this.state.data} />; }
}
const List = ({data}) => (
<ul>
{data.map(x => <li>{x}</li>)}
</ul>
);
// Use in UI:
<ListContainer />
The first thing I have to note about that is that List is actually something: it's a list of items. But ListContainer is nothing. Instead, it does things. List is a value while ListContainer is a function (and I'm talking on a semantic level. The funny thing is that List is actually a javascript function whereas ListContainer is a class, that is, a value. Quite the irony). So, let's use that observation and go deeper. We have Value Components, like List, and Action Component, like the misnamed ListContainer. A function, on a semantic level, is something that takes values and produces another values.
With Function Children Props to the rescue, we can make that very apparent and meaningful.
class Fetch extends React.Component {
componentDidMount() {
fetch('/api/data')
.then(b => b.json())
.then(b => this.setState({data: b}));
}
render() { return this.props.children(this.state.data); }
}
const List = ({data}) => (
<ul>
{data.map(x => <li>{x}</li>)}
</ul>
);
// Use in UI:
<Fetch>
{data => <List data={data} />}
</Fetch>
And Fetch is now much more reusable, it doesn't know about List because it never had to, and the semantic action of "producing a value used by the children" is more obvious than ever.
But wait. I claimed I wanted reusability. The fetched endpoint can't be specified. Let's fix that.
class Fetch extends React.Component {
componentDidMount() {
fetch(this.props.endpoint)
.then(b => b.json())
.then(b => this.setState({data: b}));
}
render() { return this.props.children(this.state.data); }
}
Better? Not quite. And I saw this kind of code in a lot of places. What's
wrong? If props.endpoint
changes, the fetching won't happen again. Sure, I
could also fetch on componentWillReceiveProps()
, but abusing the lifecycle
is not going to make things better. Instead, let's consider render()
as our
entry point and write that:
class Fetch extends React.Component {
doFetch(endpoint) {
if (this.state.endpoint === endpoint) {
return this.state.data;
}
fetch(endpoint)
.then(b => b.json())
.then(b => this.setState({data: b, endpoint: endpoint}));
}
render() { return this.props.children(this.doFetch(this.props.endpoint)); }
}
And now, props changes are reflected seemlessly. Results are cached, the component is super easy to maintain and understand, and doesn't use the complex lifecycles at all.
<Fetch endpoint="/api/data">
{data => <List data={data} />}
</Fetch>
Why limit ourselves though? Data fetching is one of many ways to produce or transform values. See a conceptual heavy use of Action Components here:
<Every seconds={10}>
{() =>
<Fetch endpoint="/api/data">
{values =>
<div>
Data: {values.join(', ')}
<ComputeMean data={values}>
{mean => `Mean is ${mean}`}
</ComputeMean>
<ComputeVariance data={values}>
{variance => `Variance is ${variance}`}
</ComputeVariance>
<ComputeHistogram data={values}>
{bins => bins.map((x, c) => `${x} appears ${c} times`)}
</ComputeHistogram>
</div>
}
</Fetch>
}
</Every>
That looks nice to me.