Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Props change does not re render #17

Open
jeff-lau opened this issue May 17, 2017 · 18 comments
Open

Props change does not re render #17

jeff-lau opened this issue May 17, 2017 · 18 comments

Comments

@jeff-lau
Copy link

As far as I know changing props doesn't cause a component to re render. Props change will trigger componentWillReceiveProps and will only cause a re render if we update the state in this life cycle method?

@NiGhTTraX
Copy link

@jeff-lau a new set of props coming in for a component will always cause it to try to re-render (I say try because you can stop rendering with shouldComponentUpdate). Think of components as functions and of props as the arguments to the functions. Receiving props is like calling the function. If you call add(1, 2) you expect 3. If you call it again with add(3, 3) you expect 6.

componentWillReceiveProps can be used to react to incoming new props and possibly trigger state changes, but a component will always try to re-render regardless if you call setState or not in that life cycle method.

@jeff-lau
Copy link
Author

jeff-lau commented Jun 4, 2017

@NiGhTTraX thank you for replying!

I find that, if a parent container changes the props passed in to a child component. The child component does not re-render automatically. The way I can get it to re-render is to update the state of the child component with the new props in the componentWillReceiveProps method.

https://stackoverflow.com/questions/38892672/react-why-child-component-doesnt-update-when-prop-changes

That seems to be the way how react behaves?
thanks,

@NiGhTTraX
Copy link

In your example you're mutating the this.props object which is not allowed because React won't notice the change. The props do indeed change but the component is unaware of this. That's why you need to forcefully trigger an update, either by calling setState or forceUpdate. Then, React will see the new value for this.props and use it.

You should never manually update this.props. If you want a component to send new props to a child component then those new props should be computed at render time e.g. as a derivation of the parent's state.

Container {
  state= { foo: 123 }
  handleEvent() {
    this.setState({ foo: 456 })
  },

  render() {
    return <Child foo={this.state.foo} />
}

Child {
  render() {
    return <div>{this.props.foo}</div>
  }
}

@swashata
Copy link

swashata commented Apr 2, 2018

Thanks @NiGhTTraX for the clarification. I am just starting with React and I wasn't sure that updating the props from outside will cause re-render (because this is what I need). I came across the stackoverflow example and I thought it is indeed not okay to change props inside a component. Your clarification clears things up.

Bottom line is I am passing props from parent to child without of course mutating them ever. So if the prop does change (from an even external component, like GrandParent) then react will re-render.

The only situation I have found to actually look for prop change is, if I am using one of the props to set an initial state. Then I would not set any state at constructor. I would rather use getDerivedStateFromProps(nextProps, prevState) and do it there. I may compare against prevState to check if it is actually necessary for me to update.

Further down I may use shouldComponentUpdate to fine tune it further.

So please correct me if my understanding is wrong. Thank you.

@NiGhTTraX
Copy link

@swashata your understanding is correct. I just want to clarify one small thing for future readers:

I would not set any state at constructor. I would rather use getDerivedStateFromProps(nextProps, prevState) and do it there

At least with the current API in 16.3.0, state needs to be defined in the constructor before calling getDerivedStateFromProps. If you don't initialize it then you get the following warning:

Warning: Foo: Did not properly initialize state during construction.
Expected state to be an object, but it was undefined.

getDerivedStateFromProps is meant to replace componentWillReceiveProps so its main purpose will be to adjust existing state based on new incoming props, not to define the initial state.

@swashata
Copy link

swashata commented Apr 2, 2018

@NiGhTTraX Thanks for the pointer. I was already setting this.state = {...} with some initial value. I did face the error when I didn't do it.

Bdw, could you please do a quick review of the codebase I am working on? I will be very glad 😅

@ryan2clw
Copy link

ryan2clw commented May 1, 2018

Great pointer about the forceUpdate when new props are passed to the component in some edge case scenarios. I may try the getDerviedStateFromProps in the future. I agree with your comment @swashata that "The only situation I have found to actually look for prop change is, if I am using one of the props to set an initial state", plus of course setting static items. I ran into a conundrum earlier today where I wanted to get the derived state from the initial props, so I did this https://stackoverflow.com/questions/50103629/passing-data-from-ajax-call-to-view-in-react-native. Seems to work for me, thought I'd pass the knowledge along. Thanks for clearing some things up for me @NiGhTTraX; reading this conversation helped validate my solution.

@bologer
Copy link

bologer commented Jul 13, 2018

@NiGhTTraX I have a small question:

I have shouldComponentUpdate() in child component. For the first render, props seems to be passed, on the state change of the parent shouldComponentUpdate() seems missing the props.

Is is somewhat expected?

@NiGhTTraX
Copy link

Hey @bologer, can you provide some example code?

@phtmgt
Copy link

phtmgt commented Jul 20, 2018

Guys, I am having a problem with re-rendering of a component (and came here Googling) on its props change:

Component rendered in parent:

<JobList project={this.state.element} jobs={this.state.activeJobs} user={this.props.user} onChange={handleChange} title="Active Jobs"/>

Change state in parent:

const handleChange = (...) => {
  ...
  this.setState({
    exclusionList: exclusionList,
    activeJobs: activeJobs,
    completedJobs: completedJobs,
  });
  ...
}

onClick method in child:

const handleClick = (job, action) => {
	      	this.props.onChange(job, action);
	    }

<button className="btn btn-icon btn-danger btn-simple btn-round btn-sm" onClick={() => handleClick({value:element.id, title: element.title, complete: element.complete}, 'removeJob')}><i className="fal fa-times"></i></button>

Set state fires for sure, as another component, that takes its props from state re-renders immediately:

<Progress value={(this.state.completedJobs.length/(this.state.activeJobs.length + this.state.completedJobs.length))*100} />

I hope I am not hijacking the thread.

EDIT: RESOLVED!

I found my (probably newbie mistake). It was very simple: I was assuming that the constructor of the child would be called whenever props is changed and was copying props to state, then using state in the render functions. Apparently, constructor of child did not run when props were changed. Once I started using props instead of state in child function everything started working.

@Microserf
Copy link

Thank you, @finkbg. I had made the exact same wrong assumption about the constructor() function running each time the properties changed. Your post helped me understand the issue I was having, and how to fix.

@solzhang777
Copy link

I think should be use Vuex, put the model object for react with parent and children components. in vue template(ui), you can bind the object (from $store.state.object). when the object changed by this.$store.commit('updateObject', newObject); your children component will be render. The means: parent, children component, even sibling components share same model object with Vuex

@bizcontact
Copy link

In your example you're mutating the this.props object which is not allowed because React won't notice the change. The props do indeed change but the component is unaware of this. That's why you need to forcefully trigger an update, either by calling setState or forceUpdate. Then, React will see the new value for this.props and use it.

You should never manually update this.props. If you want a component to send new props to a child component then those new props should be computed at render time e.g. as a derivation of the parent's state.

Container {
  state= { foo: 123 }
  handleEvent() {
    this.setState({ foo: 456 })
  },

  render() {
    return <Child foo={this.state.foo} />
}

Child {
  render() {
    return <div>{this.props.foo}</div>
  }
}

Excellent! Child should not keep this as state if it is property replication. If it is transforming then event passing and transform again.

@Guneetgstar
Copy link

@jeff-lau a new set of props coming in for a component will always cause it to try to re-render (I say try because you can stop rendering with shouldComponentUpdate). Think of components as functions and of props as the arguments to the functions. Receiving props is like calling the function. If you call add(1, 2) you expect 3. If you call it again with add(3, 3) you expect 6.

I didn't test it with react class components but whats has happened to me while using react functional components and redux changing props is this:

  const addresses = props.addresses
  const [currentAddress, setCurrentAddress] = useState(addresses && addresses[0])

I am using both the props and the state of addresses. It seems like my props didn't apply to the currentAddress when the render function is called but this could be a side effect of using useState as I had to manually change the state every time the address changes as the render function must have been called otherwise I won't be able to see my props getting changed:

  useEffect(() => {
    setCurrentAddress(addresses && addresses[0])
  }, [addresses])

@swashata
Copy link

@Guneetgstar The combination of useState and useEffect as you have done, is the right way to keep state and props in "Sync". The reason why a prop change wouldn't update your UI is

  1. When react first renders the component it (per your code) takes the addresses prop and creates a state currentAddress.
  2. When addresses is changed, then react of course re-renders your component.
  3. BUT, the useState(addresses && addresses[0]) does not take into account the value of the expression addresses && addresses[0]. Because it already has a state value from previous render.
  4. So your UI doesn't show the new value of the expression addresses && addresses[0].

But here is the thing, in your example, you do not need to create a state at all. If what you want is the value of the expression addresses && addresses[0] and show it somewhere in the UI, then why bother storing it in a state? State is supposed to be something that is local to the component, there is really no need to sync it with your props. Rather just derive what ever you want to show from the props and show it directly. Like

export function MyComponent(props) {
  const { addresses } = props;

  return (
    <div>Address is {addresses ? addresses[0] : 'Unknown'}</div>
  );
}

This is a very common mistake to think that we need to sync the state with the prop to show in the UI, I did similar mistakes earlier. I would really recommend you to read this article by @kentcdodds

https://kentcdodds.com/blog/dont-sync-state-derive-it

@Guneetgstar
Copy link

@swashata I acknowledge your suggestion but there was a need to store currentAddress in the local state as currentAddress is something that is a candidate in my UI that might be saved in redux and hence further propagated to the props. But until the user don't save the currentAddress it must be available in the UI only as user might edit it.

+1 for 3rd point.

@swashata
Copy link

swashata commented Sep 4, 2020

@Guneetgstar I see. Your use case may vary, but it is also possible to do something like

const { addresses, dispatch } = props;
const currentAddress = addresses ? addresses[0] : '';

return (
  <input type="text" value={currentAddress} onChange={e => {
    const newAddresses = addresses ? [...addresses] : [];
    newAddresses[0] = e.target.value;
    // call redux dispatch to update address
    dispatch({
      type: 'UPDATE_ADDRESS',
      payload: newAddresses,
    });
  }} />
);

But of course it would update your redux store on every keystroke. If you are mapping only needed props to your components, then it wouldn't matter much. Although if performance does become an issue then you can copy the prop to state, keep then in sync and then debounce any update to the local state through your redux dispatcher.

@Anastasiia-F
Copy link

Hello. Above in thred we have a lot clarification about why props does not trigger re-render. But the React docs tells that they should do. Why?

An update can be caused by changes to props or state.

https://reactjs.org/docs/react-component.html#updating

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests