10

Using React and Redux, imagine you have a component method that sends a request to an external API.

import React, { Component } from 'react';
import { connect } from 'react-redux';

class MyComp extends Component {

  boolUpdate (val) {
    fetch('http://myapi.com/bool', { val });
  }

  shouldComponentUpdate (nextProps) {
    return false;
  }

  render () {
    return <h1>Hello</h1>;
  }

}

const mapStateToProps = ({ bool }) => ({ bool });

export default connect(mapStateToProps)(MyComp);

Now let's say that you want to invoke boolUpdate() each time the bool prop changes, but this should not trigger a component update because nothing in the render of the component is affected.

What's the best way to do this in React?

Until recently people used to do something like:

componentWillReceiveProps (nextProps) {
  if (nextProps.bool !== this.props.bool) this.boolUpdate(nextProps.bool);
}

But as of React v16.3 componentWillReceiveProps() has been deprecated. In this example we can't use componentDidUpdate() either, because shouldComponentUpdate() prevents that from happening. And getDerivedStateFromProps() is a static method, so it doesn't have access to the instance methods.

So, the only option we're left with seems to be using shouldComponentUpdate() itself. Something along the lines of:

shouldComponentUpdate (nextProps) {
  if (nextProps.bool !== this.props.bool) this.boolUpdate(nextProps.bool);
  return false;
}

This looks rather "hacky" to me though, and not what shouldComponentUpdate() was designed for.

Does anybody have a better pattern to suggest?

Is there a preferred way to listen to specific prop changes and trigger component methods?

Thanks!

Pensierinmusica
  • 6,404
  • 9
  • 40
  • 58
  • 1
    Off-topic. The situation that you have some boolean flag in redux store but calling external API from within the component's method is very suspicious. :) – Yury Tarabanko Apr 11 '18 at 10:38
  • 1
    What if you handled this outside of the component and in the reducer instead. Each time you update the value of `bool` in state you also make your API call? – Christopher Moore Apr 11 '18 at 10:38
  • @ChristopherMoore this could be handled in a Redux middleware indeed. I just wonder if there's a good pattern to handle it directly in the component though. – Pensierinmusica Apr 11 '18 at 11:25
  • 1
    Is there a specific reason you want it handled in the component? As @YuryTarabanko mentions above, it's a code smell – Christopher Moore Apr 11 '18 at 11:33
  • The new react suspense pattern will make async call inside render :). So, no idea what the best place is. One way to do it can be to store the old value in an instance property and compare against that in componentDidUpdate – Mukesh Soni Apr 11 '18 at 11:36
  • @ChristopherMoore I have a feeling that moving component logic to middleware just because it's async sometimes makes it harder in a complex app to keep track of where things are happening. I would love if there was an elegant solution to keep this logic inside the component itself. – Pensierinmusica Apr 11 '18 at 11:36
  • @MukeshSoni yep that's a pattern that React after v16.3 seems to be leaning towards, although it doesn't work if you avoid re-rendering by returning `false` in `shouldComponentUpdate()` because then `componentDidUpdate()` is never invoked. And in this case for example re-rendering is not necessary, so it would be better to be able to avoid it. – Pensierinmusica Apr 11 '18 at 11:39
  • @Pensierinmusica in my experience, the separation of concerns actually helps in a complex app. The redux middleware contains all your actions and logic, and the components just concentrate on building the DOM – Christopher Moore Apr 11 '18 at 11:41
  • @ChristopherMoore possible, although in my case the async logic would be dependent of where the user is in the UI in that moment. So, not entirely sure. I could track the current route and use conditionals, but it seems more elegant to keep it in the component itself. Again, not sure. Also RxJS and Redux Observable seem interesting options: https://github.com/redux-observable/redux-observable – Pensierinmusica Apr 11 '18 at 11:50
  • 1
    @Pensierinmusica I think your solution with `shouldComponentUpdate` is actually the least hacky and your cleanest, logical place to do this. Next to doing it where the changed prop comes from obviously, which would be an action I assume. So why not let that action do the request? Your cornering yourself with the "in the component" constraint. It feels hacky because it's a very unusual scenario beyond the scope of a view rendering library. The props are changed by redux, and the api call is made with something that is not react i'm assuming. – timotgl Apr 11 '18 at 17:07

2 Answers2

5

If you want to run some code (e.g. data fetching) when props change, do it in componentDidUpdate.

componentDidUpdate(prevProps) {
  if (prevProps.id !== this.props.id) {
    this.fetchData();
  }
}

In your example, this won't work because shouldComponentUpdate returns false. I'd argue this is not a very common case because typically you still want to re-render if props change.

For example, if the user ID changes, you might want to show a loading indicator while the data for the new user is loading. So avoiding a re-render is not very useful in this case.

However, if you're absolutely sure you both need to prevent a re-render and need to perform a side effect like fetching data on props change, you can split your component in two. The outer component would do the data fetching in componentDidUpdate, and return <InnerComponent {...this.props} />. The inner component would have a shouldComponentUpdate implementation that prevents re-rendering further. Again, I wouldn't expect this to be a common scenario, but you can do this.

Dan Abramov
  • 264,556
  • 84
  • 409
  • 511
4

Based on the React docs and this discussion on github, The place to fetch new data based on props change is actually componentDidUpdate.

The rendering was actually splitted to two phases, the Render Phase which is pure and creates no side effects and the Commit Phase which can run side effects, work with the DOM and schedule updates.

You can see that explained well in Dan Abramov's diagram: enter image description here

Dan also mentioned:

People used to mix these two different things in componentWillReceiveProps, which is why we have to split it into a pure method (getDerivedStateFromProps) and an existing impure one where it’s okay to do side effects (componentDidUpdate).

And for the solution itself, Im attaching the example from the docs:

Fetching external data when props change

Here is an example of a component that fetches external data based on props values:

Before:

componentDidMount() {
  this._loadAsyncData(this.props.id);
}

componentWillReceiveProps(nextProps) {
  if (nextProps.id !== this.props.id) {
    this.setState({externalData: null});
    this._loadAsyncData(nextProps.id);
  }
}

After:

  static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.id !== prevState.prevId) {
      return {
        externalData: null,
        prevId: nextProps.id,
      };
    }
    return null;
  }

  componentDidMount() {
    this._loadAsyncData(this.props.id);
  }

  componentDidUpdate(prevProps, prevState) {
    if (this.state.externalData === null) {
      this._loadAsyncData(this.props.id);
    }
  }
Dan Abramov
  • 264,556
  • 84
  • 409
  • 511
Matan Bobi
  • 2,693
  • 1
  • 15
  • 27
  • This answer is meant to be more exhaustive but I think there's a simpler solution in this particular case :-) https://stackoverflow.com/a/49779063/458193 – Dan Abramov Apr 11 '18 at 15:32
  • @DanAbramov, I Totally missed that part got into it too soon. Thanks. – Matan Bobi Apr 11 '18 at 15:40
  • 1
    I took the liberty of editing your answer to match the docs more clearly—for example, you added some code to "after" that was already present in the "before" state, which made it a bit confusing. Hope that's okay, and thanks for posting the answer! – Dan Abramov Apr 11 '18 at 15:45