MUI tooltips/transitions and refs when dynamically rendering child functional components - reactjs

Trying to dynamically render components in React and having issues with MUI Tooltips and Transitions. I understand I need to pass refs down to child components, but having trouble figuring out how to in my nested functional components.
Render is called with a functional component (Component) - within Component, Tooltip or Transition are optionally rendered though functional wrapper components, then an element (Elem) is optionally rendered, then child elements are rendered through Component recursively.
Nothing I have tried has worked so far - any help on how to use refs and forward them appropriately in my case would be appreciated, thanks!
root.render(
<Component />
)
export default const Component = (props) => {
//each Elem is referencing another functional component (Label, Button, Panel, etc.)
const components = {LABEL: Label, BUTTON: Button, PANEL: Panel};
const Elem = components[component.type];
const id = props.id;
const component = useStoreValue(id);
const children = (component.children.map((child) => (
<Component id={child.id} key={child.id} />
));
return (
<TooltipWrapper showTooltip={component.attributes}>
<TransitionWrapper showTransition={component.attributes}>
<Elem attributes={component.attributes}>
{children}
</Elem>
</TransitionWrapper>
</TooltipWrapper>
)
}
//Thinking a ref needs to be created/used here and somehow passed to the children
const TooltipWrapper = (props) => {
if (props.showTooltip) {
return (
<Tooltip title={props.showTooltip}>
{props.children}
</Tooltip>
);
}
return props.children;
};
//Thinking a ref needs to be created/used here and somehow passed to the children, but this should also receive a forward ref as it can be a child of tooltip
const TransitionWrapper = (props) => {
const attributes = props.showTransition;
const Type = components[props.showTransition['animationtype']];
const components = {Collapse: Collapse, Fade: Fade, Slide: Slide};
if (props.showTransition) {
return (
<Type {...attributes}>
{props.children}
</Type>
);
}
return props.children;
};
//Example Elem - thinking this should receive a forward ref which will be set on the div element.
const Panel = (props) => {
const attributes = props.attributes;
return <div {...attributes}>{props.children}</div>;
};

Related

React child component does not re-render when props passed in from parent changes

I have a simplified react structure as below where I expect MyGrandChildComponent to re-render based on changes to the 'list' property of MyParentComponent. I can see the list take new value in MyParentComponent and MyChildComponent. However, it doesnt even hit the return function of MyGrandChildComponent. Am i missing something here?
const MyGrandChildComponent = (props) => {
return (
<div>props.list.listName</div>
);
};
const MyChildComponent = (props) => {
return (
<div><MyGrandChildComponent list={props.list}/></div>
);
}
const MyParentComponent = (props) => {
const list = { listName: 'MyList' };
return (
<div><MyChildComponent list={list} /></div>
);
}
In your MyParentComponent, the list is not a state variable and as such changing it will not even cause a re-render. If you absolutely want that when ever you change the value of list it re renders, then you will want to bring state to your functional component and the way to do that is to use hooks.
In this case your parent component will be something like below
import React, {useState} from 'react'
const MyParentComponent = (props) => {
const [list, setList] = useState({ listName: 'MyList' });
return (
<div><MyChildComponent list={list} /></div>
);
}
then at the child component you render it as I suggested in the comment above.
The parent needs to hold the list as a state variable and not just as a local variable. This is because react rerenders based on a state or prop change and at the parent you can only hold it in the state. With this when the value of list changes there will be a re-render which will then propergate the change to the children and grandchildren.
Also the only way of maintaining state in a functional component is to use hooks.
const MyGrandChildComponent = (props) => {
return (
<div>{props.list.listName}</div>
);
};
You forgot the {} around props.list.listName

Early return a component inside a react functional component with hooks

const LecturesExerciseItem = props => {
const hasMultipleVideos = hasSectionMultipleVideos(props.sectionUUID)
if (!hasMultipleVideos) return <SectionListItem {...props} />
// multiple videos
const [isAccordionOpen, setIsAccordionOpen] = useState(false)
....
return <h1> multiple videos </h1>
}
React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render.
How can early return some component based on condition inside functional component with hooks?
You have 2 options:
Put hooks on top of the component
Use a wrapper component e.g.
const LecturesExerciseItem = props => {
const hasMultipleVideos = hasSectionMultipleVideos(props.sectionUUID)
if (!hasMultipleVideos) return <SectionListItem {...props} />
// multiple videos
....
return <MultipleVideos {...props}/>
}
const MultipleVideos = props => {
const [isAccordionOpen, setIsAccordionOpen] = useState(false)
....
return <h1> multiple videos </h1>
}

React.memo - why is my equality function not being called?

I have a parent component that renders a collection of children based on an array received via props.
import React from 'react';
import PropTypes from 'prop-types';
import shortid from 'shortid';
import { Content } from 'components-lib';
import Child from '../Child';
const Parent = props => {
const { items } = props;
return (
<Content layout='vflex' padding='s'>
{items.map(parameter => (
<Child parameter={parameter} key={shortid.generate()} />
))}
</Content>
);
};
Parent.propTypes = {
items: PropTypes.array
};
export default Parent;
Every time a new item is added, all children are re-rendered and I'm trying to avoid that, I don't want other children to be re-rendered I just want to render the last one that was added.
So I tried React.memo on the child where I'll probably compare by the code property or something. The problem is that the equality function never gets called.
import React from 'react';
import PropTypes from 'prop-types';
import { Content } from 'components-lib';
const areEqual = (prevProps, nextProps) => {
console.log('passed here') // THIS IS NEVER LOGGED!!
}
const Child = props => {
const { parameter } = props;
return <Content>{parameter.code}</Content>;
};
Child.propTypes = {
parameter: PropTypes.object
};
export default React.memo(Child, areEqual);
Any ideas why?
In short, the reason of this behaviour is due to the way React works.
React expects a unique key for each of the components so it can keep track and know which is which. By using shortid.generate() a new value of the key is created, the reference to the component changes and React thinks that it is a completely new component, which needs rerendering.
In your case, on any change of props in the parent, React will renrender all of the children because the keys are going to be different for all of the children as compared to the previous render.
Please reference this wonderful answer to this topic
Hope this helps!
I was having the same issue and the solution turned out to be just a novice mistake. Your child components have to be outside of the parent component. So instead of:
function App() {
const [strVar, setStrVar] = useState("My state str");
const MyChild = React.memo(() => {
return (
<Text>
{strVar}
</Text>
)
}, (prevProps, nextProps) => {
console.log("Hello"); //Never called
});
return (
<MyChild/>
)
}
Do it like this:
const MyChild = React.memo(({strVar}) => {
return (
<Text>
{strVar}
</Text>
)
}, (prevProps, nextProps) => {
console.log("Hello");
});
function App() {
const [strVar, setStrVar] = useState("My state str");
return (
<MyChild strVar = {strVar}/>
)
}
Another possibility for unexpected renders when including an identifying key property on a child, and using React.memo (not related to this particular question but still, I think, useful to include here).
I think React will only do diffing on the children prop. Aside from this, the children prop is no different to any other property. So for this code, using myList instead of children will result in unexpected renders:
export default props => {
return (
<SomeComponent
myLlist={
props.something.map(
item => (
<SomeItem key={item.id}>
{item.value}
</SomeItem>
)
)
}
/>
)
}
// And then somewhere in the MyComponent source code:
...
{ myList } // Instead of { children }
...
Whereas this code (below), will not:
export default props => {
return (
<SomeComponent
children={
props.something.map(
item => (
<SomeItem key={item.id}>
{item.value}
</SomeItem>
)
)
}
/>
)
}
And that code is exactly the same as specifying the children prop on MyComponent implicitly (except that ES Lint doesn't complain):
export default props => {
return (
<SomeComponent>
{props.something.map(
item => (
<SomeItem key={item.id}>
{item.value}
</SomeItem>
)
)}
</SomeComponent>
)
}
I don't know the rest of your library but I did some changes and your code and (mostly) seems to work. So, maybe, it can help you to narrow down the cause.
https://codesandbox.io/s/cocky-sun-rid8o

How do I wrap this child in an Higher Order Component?

const LoggedInView = (props) => <Text>You are logged in!</Text>
export default withAuth(LoggedInView)
const withAuth = (component) => <AuthRequired>{ component }</AuthRequired>
const AuthRequired = (props) => {
const context = useContext(AuthContext)
if(!context.auth){
return (
<View style={styles.container}>
<Text>You need to login . Click here</Text>
</View>
)
}
return props.children
}
My <AuthRequired> view works fine, but my withAuth does not.
HOCs take a component and return another component. You're taking a component and returning a React node, not a component. Docs reference
In your case, you should be able to do something like:
const withAuth = (Component) => (props) => <AuthRequired><Component ...props /></AuthRequired>
It might be easier to understand as:
function withAuth(Component) {
return function WithAuthHOC(props) {
return (
<AuthRequired>
<Component ...props />
</AuthRequired>
);
}
}
You can use Auxiliary component. It is a higher order component. Auxiliary element is something that does not have semantic purpose but exist for the purpose of grouping elements, styling, etc. Just create a component named Aux.js and put this on it:
const aux = ( props ) => props.children;
export default aux;
Then wrap withAuth with Aux.
const withAuth = (component) => {
return (
<Aux>
<AuthRequired>{ component }</AuthRequired>
</Aux>
);
}
As I know, you cannot return a function as children in React. How about you trying this?
const LoggedInView = <div>You are logged in!</div>;
This code works when I tried on my laptop.
Please, take a look at this link: Functions are not valid as a React child. This may happen if you return a Component instead of from render

React.cloneElement vs render props pattern

I'm learning React and trying to figure out the best way to dynamically add props. I know two approaches how to do that, but cannot understand which one is better and faster.
First way is to use React.cloneElement api
const Parent = ({ children }) => {
const child = React.cloneElement(children, { newProp: 'some new prop' });
return child;
};
const Child = ({ newProp }) => <div>{newProp}</div>;
const App = () => (
<Parent>
<Child />
</Parent>
);
The second way is to use "render props" pattern, described on official React site: Render Props
const Parent = ({ render }) => {
const newProp = 'some new prop';
return render(newProp);
}
const Child = ({ newProp }) => <div>{newProp}</div>;
const App = () => (
<Parent render={props => <Child newProp={props} />} />
);
Both approaches give same results, but I don't know what is going on under the hood and which way to use.
React.cloneElement is a helper that's usually used to pass inline-props to avoid polluted codes. Instead of passing a prop down like this:
return <Child propA={myProps.propA} propB={myProps.propB} />
You can do this:
return React.cloneElement(Child, {...myProps})
Which is almost the same as:
return <Child {...myProps} />
The only difference here is that cloneElement will preserve previously attached refs.
Now renderProps is a technique to reuse stateful logic and has different applications than cloneElement. The first will help you with props manipulation, the second is an equivalent to High Order Components and is used whenever you want to reuse some logic to dinamically inject props into your children:
class Animation extends Component{
state = {}
componentDidMount(){/*...*/}
componentDidUpdate(){/*...*/}
render(){
const { customProps } = this.state
const { children } = this.props
return children(customProps)
}
}

Resources