ChatGPT解决这个技术问题 Extra ChatGPT

Re-render React component when prop changes

I'm trying to separate a presentational component from a container component. I have a SitesTable and a SitesTableContainer. The container is responsible for triggering redux actions to fetch the appropriate sites based on the current user.

The problem is the current user is fetched asynchronously, after the container component gets rendered initially. This means that the container component doesn't know that it needs to re-execute the code in its componentDidMount function which would update the data to send to the SitesTable. I think I need to re-render the container component when one of its props(user) changes. How do I do this correctly?

class SitesTableContainer extends React.Component {
    static get propTypes() {
      return {
        sites: React.PropTypes.object,
        user: React.PropTypes.object,
        isManager: React.PropTypes.boolean
      }
     }

    componentDidMount() {
      if (this.props.isManager) {
        this.props.dispatch(actions.fetchAllSites())
      } else {
        const currentUserId = this.props.user.get('id')
        this.props.dispatch(actions.fetchUsersSites(currentUserId))
      }  
    }

    render() {
      return <SitesTable sites={this.props.sites}/>
    }
}

function mapStateToProps(state) {
  const user = userUtils.getCurrentUser(state)

  return {
    sites: state.get('sites'),
    user,
    isManager: userUtils.isManager(user)
  }
}

export default connect(mapStateToProps)(SitesTableContainer);
you do have some other functions available, like componentDidUpdate, or probably the one your looking for, componentWillReceiveProps(nextProps) if you want to fire something when the props changes
Why do you need to re-render SitesTable if it is not changing its props?
@QoP the actions being dispatched in componentDidMount will change the sites node in the application state, which is passed into the SitesTable. The SitesStable's sites node will be changing.
Oh, I get it, i'm going to write the answer.
How to achieve this in a functional component

Q
QoP

You have to add a condition in your componentDidUpdate method.

The example is using fast-deep-equal to compare the objects.

import equal from 'fast-deep-equal'

...

constructor(){
  this.updateUser = this.updateUser.bind(this);
}  

componentDidMount() {
  this.updateUser();
}

componentDidUpdate(prevProps) {
  if(!equal(this.props.user, prevProps.user)) // Check if it's a new user, you can also use some unique property, like the ID  (this.props.user.id !== prevProps.user.id)
  {
    this.updateUser();
  }
} 

updateUser() {
  if (this.props.isManager) {
    this.props.dispatch(actions.fetchAllSites())
  } else {
    const currentUserId = this.props.user.get('id')
    this.props.dispatch(actions.fetchUsersSites(currentUserId))
  }  
}

Using Hooks (React 16.8.0+)

import React, { useEffect } from 'react';

const SitesTableContainer = ({
  user,
  isManager,
  dispatch,
  sites,
}) => {
  useEffect(() => {
    if(isManager) {
      dispatch(actions.fetchAllSites())
    } else {
      const currentUserId = user.get('id')
      dispatch(actions.fetchUsersSites(currentUserId))
    }
  }, [user]); 

  return (
    return <SitesTable sites={sites}/>
  )

}

If the prop you are comparing is an object or an array, you should use useDeepCompareEffect instead of useEffect.


Note that JSON.stringify can only be used for this kind of comparison, if it's stable (by specification it is not), so it produces the same output for same inputs. I recommend comparing the id properties of the user objects, or passing userId-s in the props, and comparing them, to avoid unnecessary reloads.
Please note that the componentWillReceiveProps lifecycle method is deprecated and will likely be removed in React 17. Using a combination of componentDidUpdate and the new getDerivedStateFromProps method is the suggested strategy from the React dev team. More in their blog post: reactjs.org/blog/2018/03/27/update-on-async-rendering.html
@QoP The second example, with React Hooks, will it unmount and remount whenever user changes? How expensive is this?
C
Chris Halcrow

componentWillReceiveProps() is going to be deprecated in the future due to bugs and inconsistencies. An alternative solution for re-rendering a component on props change is to use componentDidUpdate() and shouldComponentUpdate().

componentDidUpdate() is called whenever the component updates AND if shouldComponentUpdate() returns true (If shouldComponentUpdate() is not defined it returns true by default).

shouldComponentUpdate(nextProps){
    return nextProps.changedProp !== this.state.changedProp;
}

componentDidUpdate(props){
    // Desired operations: ex setting state
}

This same behavior can be accomplished using only the componentDidUpdate() method by including the conditional statement inside of it.

componentDidUpdate(prevProps){
    if(prevProps.changedProp !== this.props.changedProp){
        this.setState({          
            changedProp: this.props.changedProp
        });
    }
}

If one attempts to set the state without a conditional or without defining shouldComponentUpdate() the component will infinitely re-render


This answer needs to be upvoted (atleast for now) since componentWillReceiveProps is about to be deprecated and is suggested against usage.
The second form (conditional statement inside componentDidUpdate) works for me because I want other state changes to still occur, e.g. closing a flash message.
Someone had used shouldComponentUpdate in my code and I couldn't understand what's causing the problem this made it clear.
d
double-beep

You could use KEY unique key (combination of the data) that changes with props, and that component will be rerendered with updated props.


m
mr.b
componentWillReceiveProps(nextProps) { // your code here}

I think that is the event you need. componentWillReceiveProps triggers whenever your component receive something through props. From there you can have your checking then do whatever you want to do.


componentWillReceiveProps deprecated*
C
Community

I would recommend having a look at this answer of mine, and see if it is relevant to what you are doing. If I understand your real problem, it's that your just not using your async action correctly and updating the redux "store", which will automatically update your component with it's new props.

This section of your code:

componentDidMount() {
      if (this.props.isManager) {
        this.props.dispatch(actions.fetchAllSites())
      } else {
        const currentUserId = this.props.user.get('id')
        this.props.dispatch(actions.fetchUsersSites(currentUserId))
      }  
    }

Should not be triggering in a component, it should be handled after executing your first request.

Have a look at this example from redux-thunk:

function makeASandwichWithSecretSauce(forPerson) {

  // Invert control!
  // Return a function that accepts `dispatch` so we can dispatch later.
  // Thunk middleware knows how to turn thunk async actions into actions.

  return function (dispatch) {
    return fetchSecretSauce().then(
      sauce => dispatch(makeASandwich(forPerson, sauce)),
      error => dispatch(apologize('The Sandwich Shop', forPerson, error))
    );
  };
}

You don't necessarily have to use redux-thunk, but it will help you reason about scenarios like this and write code to match.


right, i get that. But where exactly do you dispatch the makeASandwichWithSecretSauce in your component?
I'll link you to a repo with a relevant example, do you use react-router with your app ?
@David would also appreciate the link to that example, I have basically the same issue.
0
0x01Brain

A friendly method to use is the following, once prop updates it will automatically rerender component:

render {

let textWhenComponentUpdate = this.props.text 

return (
<View>
  <Text>{textWhenComponentUpdate}</Text>
</View>
)

}

C
Chris Halcrow

You could use the getDerivedStateFromProps() lifecyle method in the component that you want to be re-rendered, to set it's state based on an incoming change to the props passed to the component. Updating the state will cause a re-render. It works like this:

static getDerivedStateFromProps(nextProps, prevState) {
  return { myStateProperty: nextProps.myProp};
}

This will set the value for myStateProperty in the component state to the value of myProp, and the component will re-render.

Make sure you understand potential implications of using this approach. In particular, you need to avoid overwriting the state of your component unintentionally because the props were updated in the parent component unexpectedly. You can perform checking logic if required by comparing the existing state (represented by prevState), to any incoming props value(s).

Only use an updated prop to update the state in cases where the value from props is the source of truth for the state value. If that's the case, there may also be a simpler way to achieve what you need. See - You Probably Don't Need Derived State – React Blog.