Re-render React component when prop changes - reactjs

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 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.

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

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

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.

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.

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>
)
}

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.

Related

React redux props lifecycle issue

I'm encountering a small issue for which I did fix the problem but I'm really looking for an better solution !
Let's suppose you have a Parent A component which role is to dispatch an action to fetch data.
class ParentA extends Component {
constructor(props) {
super(props)
const { dispatch } = this.props;
dispatch(actionRequest({ clientId: props.match.params.customerId }))
}
render() {
const { customer, isFetching } = this.props;
if(isFetching){
return <Spinner />
}
if(!customer){
return null
}
return <CustomerDetailsPage customerId={this.props.match.params.customerId} customer={customer} {...this.props} />
}
}
export default connect(state => ({
customer: getClient(state),
isFetching: isClientFetching(state)
}))(ParentA)
Nothing very complicated here.
Let's suppose I dispatch an action in my saga before the api call, to set isFetching to true, and one after the api call success or error to isFetching back to false.
Of course my initial state for this reducer has isFetching to false.
My action creators look like that (dont pay attention the wrapper reducer, the important thing is is the different actions )
const setFetching = isFetching => state => state.set('fetching', isFetching)
export default createReducer(initialState, {
[actionSuccess]: [setClient],
[actionRequest]: [setFetching(true)],
[actionFulfill]: [setFetching(false)],
})
The problem, to summarize is this one : when the reducer is at its initial state, there is no problem because I will put the fetched data for the first time so it is null during the first render.
The thing is about when the ParentA component unmounts, redux still store the previous value.
So when I come back to ParentA, the selector in the connect function has already a value.
Its causes at the end a useless first render of the child of ParentA since isFetching is false and customer in my exemple is not null as I just say.
In the end it causes a useless child rendering but imagine the child of ParentA fetched itself data, then it causes 2 fetches from the child !
I solved this by moving the isFetching and customer deeper in the tree, in the child but I would like to avoid splitting my props and handle this in ParentB.
I cannot memoized ParentA because the isFetching is indeed changing.
What would you eventually suggest?
I know this won't be a satisfying answer, but you would have an easier time if you were using functional components with hooks. The useEffect hook is very powerful and would 100% help you solve this problem easily. Otherwise, in Class Components, you will need to leverage lifecycle hooks like ComponentDidMount, ComponentWillUnmount etc.
I'm not sure if this is what you are looking for but these tips might help you down the line.
For your issue it would be better if you stored your data (customer & isFetching) as part of the class's state and make the action return the fetched value.
Try using lifecycle methods to handle/manipulate your state. That is instead of dispatching an action in the constructor you could do the same in componentDidMount/componentDidUpdate methods.
Try extending your class from React.PureComponent instead of React.Component. PureComponent makes your life easier by preventing unnecessary re-renders by comparing to see if the state or the props have changed. Whereas if you extend your class off a component it is up to you to manage the re-renders using the shouldComponentUpdate lifecycle method.
The below structure might help you solve your current issue.
import React, {PureComponent} from 'react'
const DEFAULT_CUSTOMER = {id: ''}
class ParentA extends PureComponent {
constructor(props) {
super(props)
this.state = {
customer: DEFAULT_CUSTOMER,
isFetching: false,
}
}
// Will be called once the component has been mounted successfully
componentDidMount() {
this.fetchCustomer()
}
//Incase your component doesn't un mount and just re-renders the content based on the client id
// you might have to use the componentDidUpdate
// will get invoked everytime the props or the state changes
componentDidUpdate(prevProps, prevState) {
const { clientId: oldClientId } = prevProps
const { clientId } = this.props
// essential for you to do some sort of check to prevent an infinite loop
if ( clientId !== oldClientId ) {
//will reset the customer to the default customer
this.setCustomer()
this.fetchCustomer()
}
}
// Set the value of isFetching
setIsFetching => (isFetching) =
this.setState({
isFetching,
})
// Set the value of customer
setCustomer => (customer = DEFAULT_CUSTOMER) =
this.setState({
customer,
})
// Dispatches the action to fetch customer
fetchCustomer => async () = {
try {
const { match, dispatch } = this.props
const { clientId } = match.params
this.setIsFetching(true)
// Assuming you are passing back the JSONIFIED response back
const resp = await dispatch(actionRequest({ clientId, }))
this.setCustomer(resp.body.customer)
this.setIsFetching(false)
} catch (e) {
this.setIsFetching(false)
}
}
render() {
const { customer, isFetching } = this.state;
if(isFetching){
return <Spinner />
}
if(!customer.id){
return null
}
return <CustomerDetailsPage customerId={this.props.match.params.customerId} customer={customer} {...this.props} />
}
}
export default ParentA

componentWillReceiveProps not called after redux dispatch

I'm building a react native app and using redux to handle the state. I am running into a situation where one of my containers is not updating immediately when the redux state is changed.
Container:
...
class ContainerClass extends Component<Props, State> {
...
componentWillReceiveProps(nextProps: Object) {
console.log('WILL RECEIVE PROPS:', nextProps);
}
...
render() {
const { data } = this.props;
return <SubComponent data={data} />
}
}
const mapStateToProps = (state) => ({
data: state.data
};
export default connect(mapStateToProps)(ContainerClass);
Reducer:
...
export default function reducer(state = initalState, action) => {
switch(action.type) {
case getType(actions.actionOne):
console.log('SETTING THE STATE');
return { ...state, data: action.payload };
...
...
...
In a different random component, I am dispatching a call with the actionOne action, which I confirm prints out the relevant console.log. However, the console.log in the componentWillReceiveProps in the container is not printed.
The component that dispatches the call is a modal that has appeared over the Container, and closes automatically after the call is dispatched and the state is updated. What is weird is that although the Container isn't updated immediately, if I navigate to a different page and then back to the Container page, the state is in fact updated.
EDIT: Initial state is:
const initialState: Store = {
data: []
}
And the way I dispatch is in a different component which gets called as a new modal (using react-native-navigation) from Container:
fnc() {
...
setData(data.concat(newDatum));
...
}
Where setData and data are the redux dispatch action and the part of the store respectively that is passed in on props from the Container (which has setData and data through mapStateToProps shown above and a mapDispatchToProps which I didn't show).
I solved my problem by updating from react-native v0.56 to v0.57. Apparently there was a problem with react-redux v6 working properly in the react-native v0.56 environment.
Assuming you're using a recent version of React, componentWillReceiveProps is actually deprecated:
Using this lifecycle method often leads to bugs and inconsistencies
You can't really rely on that lifecycle hook in a number of situations. You may want to look at a slightly different approach with componentDidUpdate instead.
I think more important is to get the value after changing in state of redux rather than in which lifecycle you are getting the value . so for getting the value you can use subscribe method of redux in componentDidMount
store.subscribe( ()=> {
var updatedStoreState = store.getState();
})
I believe that getDerivedStateForProps would solve your problem.
static getDerivedStateFromProps(nextProps, prevState) {
if(nextProps.data !== prevState.data) {
//Do something
} else {
//Do something else
}
}
You would check the state from the redux against the state from your component and then act accordingly.
Also, some info from the documentation that you might consider before using this method:
1. getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates.
2. This method exists for rare use cases where the state depends on changes in props over time.
3. If you need to perform a side effect (for example, data fetching or an animation) in response to a change in props, use componentDidUpdate lifecycle instead.
You can read more at: https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops

Manage props changes in react native

I m building an appointment application with react native,its my first try in RC,Using react-redux to manage the state.I need some clarification about lifecycle methods.
Details.js
class Details extends Component {
constructor(props) {
super(props);
}
componentDidMount(){
this.props.bookingAndSuggestions(Data)
}
componentWillReceiveProps(nextProps){
if( Object.keys(nextProps.bookingSuggestionStatus).length >0) {
if(nextProps.bookingSuggestionStatus.res.data.status=='available') {
this.setState({isAvailable:false})
} else {
this.setState({isAvailable:true})
}} }
onBookNow=()=>{
this.props.shops(Data);
}
Here is the deal,Initially i call react-redux action prop this.props.bookingAndSuggestions(Data) and i capture the response inside componentWillReceiveProps,and On booking this.props.shops(Data); will trigger and it also updates the componentWillrecieveprops,the logic inside the componentwillrecieveprops updates each time when props changes.What is the proper approach to deal with such situation?
componentWillReceiveProps is called not only when the props change but also when the parent re-renders and hence whenever it is being called the state is being evaulated and set again.
You have two options
If you aren't modifying the isAvailable state internally, you could simply use it from props directly.
Eg:
const isAvailable = this.props.bookingSuggestionStatus && this.props.bookingSuggestionStatus.res.data.status=='available'
If you are modifying it, then you need to check if the props have changed which you can do in componentWillReceiveProps(use getDerivedStateFromProps from v16.3.0 onwards)
Eg:
componentWillReceiveProps(nextProps){
if(!_.isEqual(nextProps.bookingSuggestionStatus, this.props.bookingSuggestionStatus) && Object.keys(nextProps.bookingSuggestionStatus).length >0) {
if(nextProps.bookingSuggestionStatus.res.data.status=='available') {
this.setState({isAvailable:false})
} else {
this.setState({isAvailable:true})
}
}
}

ComponentDidMount not getting called after redux state update?

I think I'm missing a concept here about React and Redux. I'm trying to work with objects stored in redux, and I'm having trouble.
REDUX:
I have an action fetchItems, that gets all items from the database. This action works successfully.
REACT:
I have a container, UserProfile, that calls fetchItems in componentDidMount.
class UserProfile extends Component {
componentWillMount() {
console.log('------------ USER PROFILE -------------------');
}
componentDidMount() {
console.log('[ComponentDidMount]: Items: ', this.props.items);
this.props.fetchItems();
}
render() {
let profile = null;
console.log('[Render]: Items: ', this.props.items);
return <Auxillary>{profile}</Auxillary>;
}
}
const mapStateToProps = state => {
return {
items: state.items.items
};
};
const mapDispatchToProps = dispatch => {
return {
fetchItems: () => dispatch(actions.fetchItems())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(UserProfile);
The problem I'm seeing is that this.props.items is always null (even though fetchItems is successful). The only way I can detect that items were stored in redux store is if I use componentWillRecieveProps(nextProps). Here, I successfully see the items in nextProps. I feel like using componentWillReceiveProps might be too "messy" though. I guess what I'm asking is, what is the standard way of dealing with updates to redux states in react?
Aseel
The cycle will be :
constructor()
componentWillMount() (will be soon deprecated by the way : https://medium.com/#baphemot/whats-new-in-react-16-3-d2c9b7b6193b)
render() => first render (this.props.items, coming from mapStateToProps will be undefined)
componentDidMount() => launching fetchItems() => changing redux state => changing the this.props.items => launching the second render() where this.props.items will be set.
So :
you should have two console.log('[Render]: Items: ', this.props.items);
you should deal with a "loading" state when the this.props.items is null
If the second console.log is still null, Try to add log in your reducer, in the mapStateToProps, ... perhaps it's not state.items.items ...
In react, we have something called state. if the state of a component is changed the component will re-render. Having said that we can use this.setState() inside componentWillRecieveProps to update the state which in turn will rerender the component. So your code will look like this which is the standard way to handle Redux level state changes in react.
class UserProfile extends Component {
constructor(props) {
super(props);
this.state = {
items: props.items
}
}
componentWillMount() {
console.log('------------ USER PROFILE -------------------');
}
componentWillRecieveProps({ items }) {
this.setState({ items });
}
componentDidMount() {
console.log('[ComponentDidMount]: Items: ', this.state.items);
this.props.fetchItems();
}
render() {
let profile = null;
console.log('[Render]: Items: ', this.state.items);
return <Auxillary>{profile}</Auxillary>;
}
}
const mapStateToProps = state => {
return {
items: state.items.items
};
};
const mapDispatchToProps = dispatch => {
return {
fetchItems: () => dispatch(actions.fetchItems())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(UserProfile);
P.S Just making the API call inside componentWillMount will not help either as API call is async and can take up some time to resolve and till then react will finish rendering the component. so you'll still have to use componentWillRecieveProps
Standard practice is to call this.props.fetchItems() in your constructor or componentWillMount().
componentDidMount is called after render which is why your items do not render - they do not exist until after the initial render.
There are certain ways you can resolve this.
The very first time when render() gets called it was subscribed to the initial props/state that was initialise in redux store through redux connect method. In your case items was null.
Always initialise your redux store with some meaningful data.
In your case if items will be array you can initialise with empty array.
When you dispatch action your store will get updated and the component which was subscribed to items will be re rendered and in this way you donot have to use setState inside componentWillReceiveProps and you can avoid using it.
You need to handle certain cases in render like if array is empty and data is still loading then show some kind of loader and once data is fetched then display it.

Redux & React: componentDidUpdate is called but rerendering doesn't happen

I am loading data from Rest API, Container and Presentational components take it.
Container
componentWillMount() {
this.props.load();
}
componentDidUpdate() {
console.log('updated');
}
render() {
let view;
view = this.props.data ? (<Foo data={this.props.data} />) : (<H2>Loading</H2>)
return (
<div>
{view}
</div>
)
}
load - fetches data and dispatched event in reducer on success load.
#Connect
function select(state) {
const { data } = state.modules;
return {
data: data
};
}
export default connect(select, {
load: actions.data.load
})(ContainerComponent);
As far as I understand, when action is dispatched and Container component receives updated data from store, rerender should happen.
Which is strange, componentDidUpdate, according to the docs, is called when component received updated props and rerendered.
But my Foo (dumb) component never shows up even though everything is successfully dispatched without state mutation.
What could be the cause? Thanks!
One of the most common reasons for a component not to re-render is that you modify the state instead of returning a copy of the state with changes applied.
This is mentioned in the Troubleshooting part of the Redux docs.
As #kjonsson was hinting, the issue is that you directly assign state.modules.data to the data property on the select object. The reference of that data object never changes and thus the component will never re-render. What you have to do is copying state.modules.data using the spread operator, such that whenever state.modules.data changes the data field on the select object will have a new reference.
Below the adjusted code snippet.
function select(state) {
const { data } = state.modules;
return {
data: { ...data }
};
}
export default connect(select, {
load: actions.data.load
})(ContainerComponent);
Please let me know if this helps!

Resources