Reputation: 3098
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
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
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
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
Reputation: 1341
useEffect(() => {
// anything you want to do
, [JSON.stringify(dependencyName)]}
Upvotes: -2