2

I want to create a component Person that is fully controlled by its state. It should also be able to sync the props change (firstName, lastName) passed from its parent component to the state. I have the following code. It does what I want which is syncing props to state and re-render after state has been changed.

However one issue I noticed is that useEffect gets invoked after DOM update triggered by the parent props change. So the initial re-render is wasted since I only want it to re-render after useEffect gets invoked and state is changed.

import React,{useState, useEffect} from 'react';

const Person = ({firstName, lastName}) =>  {
   const [name, setName] = useState(firstName + lastName)

   useEffect(() => {
       setName(firstName + lastName);
       console.log("state changed!");
   }, [firstName, lastName])

   console.log("re-render!");
   return <div>render {name}</div>;
}

export default Person;

I created a simple demo here https://codesandbox.io/s/sparkling-feather-t8n7m. If you click the re-render button, in the console you will see below output. The first re-render! is triggered by props change which I want to avoid. Any idea how to achieve this? I know there are other solutions such as making it fully uncontrolled, but I'd like to know if there is any workaround to this solution

re-render!
state changed!
re-render!
user3908406
  • 1,416
  • 1
  • 18
  • 32
  • would it work fo you if on first mount the useEffect code does not get executed? – Jagrati Apr 29 '20 at 17:02
  • The issue is more about how to prevent props changes from its parent from causing re-render. The end goal is to make it so that props changes only invoke useEffect which update the state based on props, not re-render the component. Re-rendering should be triggered only after state changes – user3908406 Apr 29 '20 at 17:16

2 Answers2

1

you will need to add a condition in your useEffect. something like :

const [didUpdate, setDidUpdate] = useState(false);

useEffect(() => {
  if(didUpdate){
    setName(firstName + lastName);
    console.log('state changed!');
  } else {
    setDidUpdate(true);
  }
}, [firstName, lastName]);

Here it reproduce the componentDidUpdate() behavior.

On the first rendering, component is mounted, didUpdate is initialised to false, so the effect will only set it to true for the next updates.

Note that a state (useState) initialised with a props isn't updated when the prop changes.

Quentin Grisel
  • 4,794
  • 1
  • 10
  • 15
  • Thanks, wouldn't the first re-render caused by parent props change still occur? – user3908406 Apr 29 '20 at 17:05
  • You will have to test that, it's a good question but since your parent is re-rendering, I assume the child are as well. So it would result in a first rendering for your child and so wouldn't trigger the effect. Let me know when you test it I am curious :) – Quentin Grisel Apr 29 '20 at 17:09
  • Looks like i was wrong. https://stackoverflow.com/a/40820657/9868549 here someone says that your children won't be re-rendered automatically if the parent update. so it doesn't change the fact that if your parent render, your child will remain as it was, with a `didUpdate = true` (Since they were already rendered once before) – Quentin Grisel Apr 29 '20 at 17:15
  • the issue in that link is when child component is re-rendered when props has not changed – user3908406 Apr 29 '20 at 17:21
  • Yeah i was just talking about the beginning of his answer where he explain how rendering the parent affect the children – Quentin Grisel Apr 29 '20 at 17:28
0

It's basically React core behavior, according to doc

The function passed to useEffect will run after the render is committed to the screen.

in order to avoid bugs caused by side effects.

I guess you simplified your example a bit, but you should know that copying props to state unconditionally is an anti-pattern https://en.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#anti-pattern-unconditionally-copying-props-to-state

Instead, you should directly use props :

const Person = ({firstName, lastName}) => (
  <div>render {firstName + lastName}</div>;
);
  • My example is over simplified. I have logic in the component to update the state without affecting its parent, but also want to take in the changes from parent – user3908406 Apr 29 '20 at 17:18
  • You will never update parent from child (React data flow is unidirectional). Can you post the full component ? –  Apr 29 '20 at 17:28
  • My component is complex. You can replace `return
    render {name}
    ` with `return setName(e.target.value)}>`. as a further example. So the component's state will update both from the props passed by its parent or the interaction on the input. The props from parent should not re-render the component but only update the state
    – user3908406 Apr 29 '20 at 18:32
  • Okay so in my opinion, what's wrong here is that you both use parent's props and local state to manage `name`. Here,`Person` il NOT fully controlled, because it has a local state. A fully controlled component is a component that only update using parent's props. What you should do if you want a fully controlled component is to do the following : –  Apr 30 '20 at 07:37
  • const Person = ({ name, onChange }) => { ... return (
    ); } and use onChange to update parent state, where you store `name` that you pass to `Person`
    –  Apr 30 '20 at 07:42