ManavM
ManavM

Reputation: 3098

Default Props in stateful (using hooks) functional components

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?

Upvotes: 6

Views: 5330

Answers (4)

Fraction
Fraction

Reputation: 12954

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>

Upvotes: 0

McKabue
McKabue

Reputation: 2222

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')
);

Upvotes: -1

Ansal Ali
Ansal Ali

Reputation: 1603

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!

Upvotes: 8

Muhammad Haseeb
Muhammad Haseeb

Reputation: 1341

useEffect(() => {
// anything you want to do
, [JSON.stringify(dependencyName)]}

Upvotes: -2

Related Questions