Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

  • React - defaultProps vs ES6 default params when destructuring (performances issues)
  • React functional component default props vs default parameters

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?

like image 983
ManavM Avatar asked Jan 30 '20 11:01

ManavM


1 Answers

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!

like image 116
Ansal Ali Avatar answered Sep 19 '22 17:09

Ansal Ali