Suppose I have these components in my project:
class MessageHeader extends React.Component { /* ... */ }
class NiceButton extends React.Component { /* ... */ }
class FridgeContents extends React.Component { /* ... */ }
All of these components are presentational, and take a timestamp: number
as a props. I pass this timestamp prop to them, but only once, so basically it's like a constant.
Inside these components, I use the function humanTime
which does a presentational thing: converts 1512767145540
to 2 minutes ago
. I want to periodically recall this function so that it changes the rendering to 3 minutes ago
and so forth. For simplicity, assume I'm just calling humanTime(123)
inside render methods.
The dumbest way of solving this is implementing that logic in every component's lifecycle hooks:
class MessageHeader extends React.Component {
constructor(props) {
super(props)
}
+ componentDidMount() {
+ this.interval = setInterval(() => this.forceUpdate(), 30e3);
+ }
+ componentWillUnmount() {
+ clearInterval(this.interval);
+ }
render() {
// calls humanTime(123)
// ...
}
}
class NiceButton extends React.Component {
constructor(props) {
super(props)
}
+ componentDidMount() {
+ this.interval = setInterval(() => this.forceUpdate(), 30e3);
+ }
+ componentWillUnmount() {
+ clearInterval(this.interval);
+ }
render() {
// calls humanTime(123)
// ...
}
}
class FridgeContents extends React.Component {
constructor(props) {
super(props)
}
+ componentDidMount() {
+ this.interval = setInterval(() => this.forceUpdate(), 30e3);
+ }
+ componentWillUnmount() {
+ clearInterval(this.interval);
+ }
render() {
// calls humanTime(123)
// ...
}
}
Yes, this is a solution, but a pretty horrible one when it comes to DRY (Don't Repeat Yourself) and maintenance and legibility of purpose
I want to compose this logic into my three (or many) components, so I can simply write
const MessageHeader2 = withPeriodicRefresh(MessageHeader);
const NiceButton2 = withPeriodicRefresh(NiceButton);
const FridgeContents2 = withPeriodicRefresh(FridgeContents);
And then just use those components, which now have that setInterval
/forceUpdate
logic. I don't necessarily want to insert any child component, nor a wrapper component. I just want to add this logic into all those components. Mutating the input component is one solution, but that's not the best for composability and predictability.
So ideally, I just want MessageHeader2
to be a new component basically behaves like MessageHeader
but has periodic refreshing built into it. It puts all that Solution Zero stuff into a utility and I reuse that. Simple.
Inheritance: MessageHeader2 extends MessageHeader
. This works, but Java has taught us that at some point you will get stuck wishing to do multiple inheritance, which isn't reliable due to the Diamond Problem In Inheritance.
Wrapper component or higher-order component
class PeriodicRefresh extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.interval = setInterval(() => this.forceUpdate(), 1e3);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return this.props.render();
}
}
And its usage:
<PeriodicRefresh render={() => <MessageHeader name={"World"} />} />,
This actually doesn't work because the MessageHeader may legitimately have an optimized shouldComponentUpdate:
class MessageHeader extends React.Component {
constructor(props) {
super(props)
}
// HERE
shouldComponentUpdate(nextProps) {
return nextProps.name !== this.props.name;
}
render() {
return <h2>Hello {this.props.name} {humanTime(123)}</h2>
}
}
Which blocks the forceUpdate
from PeriodicRefresh because now there is a parent-child component boundary. This is all "logical" and abides by the design principles, but by introducing a parent-child boundary, I lose the capability of calling forceUpdate
(which is to bypass shouldComponentUpdate) as a feature in my composable utility. So a wrapper component is not even a solution at all.
Introduce a new prop whatever
to MessageHeader
, the prop would cause the re-render. A wrapper component would pass this prop to the MessageHeader
.
There are three problems with this approach:
- It requires us to update
MessageHeader
shouldComponentUpdate
, which is not composability (see section "Desired solution") - It still requires copy-pasting code to
MessageHeader
,NiceButton
,FridgeContents
- It is not clear, to a colleague or to your future self, what is the prop
whatever
for, since its contents aren't actually used for the rendering
Recognize that Date.now()
inside the library humanTime
is the source of updates, extract that from the library, pull it all the way up the tree to somewhere in state management architecture or in props.
I can explain why that is an imperfect solution. To begin with, this whole problem is perfectly solvable by sprinkling setInterval
with forceUpdate
in lifecycle hooks (see Solution zero above). That's a low-cost solution, but it's a bad idea for code reusability and maintenance.
A rearchitecture like this solution num 4 involves insight into third-party libraries, rework of component hierarchy and their props, and/or rework near state management or controller components. That's a high-cost solution.
It should be cheap to do the right thing, and expensive to do the wrong thing, but in this case solution num 4 turns it around, and makes solution zero more attractive to developers. That's probably what often happens in the end of the day.
refs
function withPeriodicRefresh(Comp) {
return class extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.interval = setInterval(() => {this.child.forceUpdate()}, 1e3);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return <Comp {...this.props} ref={child => this.child = child} />
}
}
}
This actually works.
But using refs is not recommended, said Andrew Clark on Twitter. https://twitter.com/acdlite/status/939219265332289536 I can understand how refs are "not so nice" for updating children, but in this case I didn't want to introduce children in the first place. One could say this is a perfectly valid use case for refs, but there is documentation-induced shame for using them, or just labeling this as an escape hatch.
Apart from the "against best practices" issues of using refs, there is also some subtle limitations: https://reactjs.org/docs/refs-and-the-dom.html#refs-and-functional-components
// This is a functional component, it doesn't support refs
function MessageHead(props) {
return <h2>Hello {props.name} {humanTime(123)}</h2>;
}
const MessageHeader2 = withPeriodicRefresh(MessageHead);
Suddenly we stop losing predictability. Some components behave fine when we pass them to withPeriodicRefresh
, and other components don't work at all.
So yeah, I am not aware of any solution for implementing withPeriodicRefresh(Foo)
(from "desired solution" section) which ticks all these boxes:
- Works no matter what component you pass as input
Foo
(predictability) - Requires no changes to the implementation of
Foo
(separation of concerns & unleaky abstraction) - Requires no new prop on
Foo
(unleaky abstraction) - Is cheap compared to solution zero
Streams?