2

I'm dynamically generating children components of HOC parent (see below). I pass the props directly to one of children and set the prop in it. I expect to see child re-rendering on props change but it doesn't.

Is the code incorrect somewhere?

ParentComponent

...
const ParentComponent = ({children}) => {
   const [state1, setState1] = useState(true);

   ...

   const changeOpacity = event => setState1(!state1);

   const renderChildren = React.useCallback(() => React.Children.toArray(children).map((child, index) => (
      <div key={index} style={{opacity: `${state1 ? 0 : 1}`}}>
         {child}
      </div>
   )), [state1]);

   return (
      <div>
         <Button onClick={changeOpacity}>Toggle Opacity</Button>
         {renderChildren()}
      </div>
   );
};

App.js

...
const App = () => {
   const [prop1, setProp1] = useState(123);

   return (
      <ParentComponent>
         <Child1 prop1={prop1} setProp1={setProp1} />
         <Child2 />
      </ParentComponent>
   ); 
};
Waleed93
  • 1,130
  • 2
  • 15
  • 24
  • Isn't that because of your `state1` doesn't change, so you get momoized `renderChildren`? What will happen if you skip `useCallback()`? – Yevhen Horbunkov Jun 10 '21 at 10:48
  • children re-renders when only Toggle Opacity button click. because the renderChildren depends on state1 and nothing else – ilkerkaran Jun 10 '21 at 11:10
  • Hmm... I thought child component renders irrespective of ```state1``` mutation when its props changes. But yeah removing ```useCallback``` hook makes it work. – Waleed93 Jun 10 '21 at 11:16

2 Answers2

2

In your ParentComponent, the children are cloned and then used to render as a part of the return value from the renderChildren function. Since the logic to compute children is not run on change of props to children, your child component is not affected by a change in its prop.

You can add children dependency to useCallback and it will work fine.

const { useState, useCallback } = React;
const ParentComponent = ({children}) => {
   const [state1, setState1] = useState(true);


   const changeOpacity = event => setState1(!state1);

   const renderChildren = useCallback(() => React.Children.map(children, (child, index) => (
      <div key={index} style={{opacity: `${state1 ? 0 : 1}`}}>
         {child}
      </div>
   )), [children, state1]);

   return (
      <div>
         <button onClick={changeOpacity}>Toggle Opacity</button>
         {renderChildren()}
      </div>
   );
};
const Child1 = ({prop1, setProp1}) => <div>{prop1} <button onClick={() => setProp1(234)}>Click</button></div>;
const Child2 = () => <div>Hello</div>
const App = () => {
   const [prop1, setProp1] = useState(123);

   return (
      <ParentComponent>
         <Child1 prop1={prop1} setProp1={setProp1} />
         <Child2 />
      </ParentComponent>
   ); 
};

ReactDOM.render(<App />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="app" />
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • Yeah, ```children``` dependency makes it work. But my previous thinking was the child would re-render regardless of ```state1``` when its props changes. I overkilled the logic by using the hook. Better to only use it if parent re-renders frequently and there's no point rendering the children every time. – Waleed93 Jun 10 '21 at 11:30
  • Yes, that is correct. You can avoid useCallback too if your parent component is not rendering frequently – Shubham Khatri Jun 10 '21 at 11:36
0

Is there anything prevent you from the approach below;

const ParentComponent = ({children}) => {
   const [state1, setState1] = useState(true);

   ...

   const changeOpacity = event => setState1(!state1);

   const renderChildren = useCallback(() => React.Children.toArray(children).map((child, index) => (
      <div key={index}>
         {child}
      </div>
   )), [children]);

   return (
      <div>
         <Button onClick={changeOpacity}>Toggle Opacity</Button>
         {state1 && renderChildren()}
      </div>
   );
};
ilkerkaran
  • 4,214
  • 3
  • 27
  • 42