6

I went through several questions on SO regarding default props for functional components and they all recommend using ES6 default parameters. Here are links to those questions.


However, when I use that method for writing components with effects running on props change, I get unwanted behaviour with non-primitives. For example, the following code will result in an infinite loop.

const Parent = () => {
  let somethingUndefined;

  return (
    <div>
      <Child prop={somethingUndefined} />
    </div>
  );
};

const Child = ({ prop = {a: 1} }) => {
  const [x, setX] = React.useState(1);

  React.useEffect(() => {
    setX(x + 1);
  }, [prop]);

  return <div>{x}, {prop.a}</div>;
};

ReactDOM.render(<Parent />, document.getElementsByTagName('body')[0]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.11.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script>

I attempted two ways of attempting to circumvent the issue. First, by just assigning a different variable that contains the default, and putting the unmodified prop in the dependency array. ie

const Child = ({ prop }) => {
  const [x, setX] = React.useState(1);

  const defaultedProp = prop || {a: 1};

  React.useEffect(() => {
    setX(x + 1);
  }, [prop]);
  // Note we use prop and not defaultedProp here to avoid runnning into the issue above.

  return <div>{x}, {defaultedProp.a}</div>;
};

Another method would be to just use something like (prop || {a:1}) in place of prop everywhere you use it, except in the dependency array. ie

const Child = ({ prop }) => {
  const [x, setX] = React.useState(1);

  React.useEffect(() => {
    setX(x + 1);
  }, [prop]);

  return <div>{x}, {(prop || {a: 1}).a}</div>;
};

But both of these solutions seem suboptimal since it would require a lot of wasted effort (and bulky code).

defaultProps is also a solution to the infinite loop issue but it is deprecated. Note that the example provided in this rfc also uses ES6 default parameters in the code.

Am I missing something? Is there a better way to use default props in stateful functional components that run effects on props change?

ManavM
  • 2,918
  • 2
  • 20
  • 33
  • Why not pass default props directly from parent itself? – kooskoos Jan 30 '20 at 11:19
  • @Then that would be the default behaviour for the `Parent` component. We would want the `Child` component to have these defaults (Since a child component would most likely be reusable and need the same set of defaults everywhere). – ManavM Jan 30 '20 at 11:27
  • Long live `defaultProps`. – KFunk Nov 02 '20 at 22:51

4 Answers4

8

I don't know whether this is eligible for an answer but all your concerns could be resolved by declaring your default value as a constant in the app. That means;

const Parent = () => {
  const somethingUndefined;

  return (
    <>
      <Child prop={somethingUndefined} />
    </>
  );
};

const Child = ({ prop = {a: 1} }) => {
  const [x, setX] = React.useState(1);

  React.useEffect(() => {
    setX(x + 1);
  }, [prop]);

  return <div>{x}, {prop.a}</div>;
};

You can change the above code to

const Parent = () => {
  const somethingUndefined;

  return (
    <>
      <Child prop={somethingUndefined} />
    </>
  );
};

const defaultPropValue = {a: 1};

const Child = ({ prop = defaultPropValue }) => {
  const [x, setX] = React.useState(1);

  React.useEffect(() => {
    setX(x + 1);
  }, [prop]);

  return <div>{x}, {prop.a}</div>;
};

This will not cause any infinite loops.

The difference bet these two:- In the first, the prop is initialized to a new value ie, {a: 1} and on every state update, this will be a new object (the new object will be in a new memory location), and it invokes the callback again.

In the second, we initialized and assigned the {a: 1} to defaultPropValue which will not change. Then we assigned this defaultPropValue to prop so that on every re-render, the value assigned to the prop will be the same ( or from the same memory location). So it works as expected.

Hope the idea is clear!

Ansal Ali
  • 1,583
  • 1
  • 13
  • 30
  • This works as well as I can hope for I suppose. It's still fairly tedious, since you have to define some like `childDefaults` for every stateful functional component that needs to have a default (which is all of them if you want a robust app). But this seems to be the best solution yet. – ManavM Feb 04 '20 at 10:35
0

The useEffect() will run the first time and invoke the setX() then:

  • setX() will update the state of x which will trigger the component to re-render again.
  • prop will receive a new object const Child = ({ prop = {a: 1} }) => {
  • useEffect() will run again and invoke the setX()

the whole process repeats again, This causes an infinite loop.

Instead you could pass a default value to a property and use it in the useEffect() dependencies array

const Parent = () => {
  let somethingUndefined; // babel complains if we use `const` without value

  return (
    <div>
      <Child prop={somethingUndefined} />      
      <Child prop={{ a: 3 }} />
    </div>
  );
};

const Child = ({ prop = {} }) => {
  const { a = 1 } = prop;
  const [x, setX] = React.useState(1);

  React.useEffect(() => {
    setX(x + 1);
  }, [a]);

  return <div>{x}, {a}</div>;
};

ReactDOM.render(<Parent />, document.getElementsByTagName('body')[0]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.11.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script>
Fraction
  • 11,668
  • 5
  • 28
  • 48
  • Yeah I realised the re-instantiation of the object was causing the loop. However, Your solution is basically the same as putting `prop.a` in the dependencies or writing `const {a = 1} = prop;`. Which can get very tedious, since in a real world situation you would end up having to list out every primitive in the object that is subject to change. – ManavM Feb 03 '20 at 07:55
-1

See https://codepen.io/McKabue/pen/dyPxGLQ?editors=0010

const Parent = () => {
  const somethingUndefined = undefined;

  return <Child prop={somethingUndefined}/>;
};

const Child = ({ prop = {a: 1} }) => {
  const [x, setX] = React.useState(1);

  React.useEffect(() => {
    setX(prop.a + 1);
  });

  return <div>{x}, {prop.a}</div>;
};


ReactDOM.render(
  <Parent />,
  document.getElementById('root')
);
McKabue
  • 2,076
  • 1
  • 19
  • 34
-2
useEffect(() => {
// anything you want to do
, [JSON.stringify(dependencyName)]}
Muhammad Haseeb
  • 1,269
  • 12
  • 22
  • 2
    according to [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) `JSON.stringify` does not preserve key order within an object. This would mean that you could potentially get an infinite loop since the string would keep changing due to different key orders. – ManavM Jan 30 '20 at 11:40
  • Yes thats true, in that case you can destructure your object and save the specific key in a variable for which you want to take the effect, and add that variable into the dependency – Muhammad Haseeb Jan 30 '20 at 11:43
  • 1
    That would work, with the assumption that the value of that key would be a primitive. But if that were the case the bug would not exist in the first place. That is, I could just as easily right `useEffect(()=> {}, [prop.a])` since it would be equally effective in preventing the bug as writing `useEffect(()=> {}, [JSON.stringify(prop.a)])`. The problem occurs when the dependency needs to be a non-primitive value, in which case the above solution could possibly still cause an infinite loop. – ManavM Jan 30 '20 at 11:52