Skip to content

Instantly share code, notes, and snippets.

@staltz
Last active October 11, 2022 05:48
  • Star 20 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save staltz/08bf613199092eeb41ac8137d51eb5e6 to your computer and use it in GitHub Desktop.

Logic composability problems of lifecycle hooks in React

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.

Solution zero

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

Desired solution

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.

Imperfect solution 1

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.

Imperfect solution 2

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.

Imperfect solution 3

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

Imperfect solution 4

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.

Imperfect solution 5

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.

Conclusion

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
@skrivle
Copy link

skrivle commented Dec 9, 2017

If you'd like the create stateful component using a more functional api, I'm working on React Stateful Component. It guides you into having a pure render function by avoiding the usage if this and by passing props and state as parameters. It's inspired on ReasonReact's api and essentially a lightweight wrapper around React's class components. Not entirely finished yet though.

@seekshiva
Copy link

seekshiva commented Dec 9, 2017

@staltz <HumanTime time={time} /> also has another advantage. Depending on how far away props.time is from current time, you can set the interval accordingly.

If the difference is less than a few minutes, you can make the setInterval happen every 1 second. If it is more than 2-3 hours, you can make the setInterval execute every 30 minutes. If it is more than a day old, you can set a precise timeout of when to update, instead of setInterval. A post that was created '3 days ago' is going to change to '4 days ago' after a certain time, you can initiate setTimeout for that specific time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment