How to get a specific child in a React component? - javascript

In the simple example below, I'm looking for a way to get for example CardBody child.
const Card = ({children}) => {
const cardHeadChild = ...;
const cardBodyChild = ...;
return (
<div>
{cardHeadChild}
{cardBodyChild}
</div>
)
}
const CardHead = () => <div>Head</div>
const CardBody = () => <div>Body</div>
// Usage:
<Card>
<CardHead>
<CardBody>
</Card>
I cannot get by index (eg: React.Children.toArray(children)[1]) because children are optionals.
I tried something like this:
React.Children.forEach(children, child => {
if(child.type.name === 'CardBody') cardBodyChild = child
// or
if(child.type.displayName === 'CardBody') cardBodyChild = child
..
})
but it doesn't work when component are wrapped in HOC.
Any solution ?

Function name shouldn't be used in production client-side code because function names are mangled when the application is minified. The same applies to displayName - unless it was set explicitly. Also notice that primary use of displayName and name is debugging.
Children can be identified by React element type. If the purpose is to output optional children in specified order, this can be done similarly to this answer:
Optional head:
{props.children.find(({ type }) => type === CardHead)}
Optional body:
{props.children.find(({ type }) => type === CardBody)}
It's expected that children should be exactly CardHead and CardBody stateless components. If there's a need to enhance their functionality with other components, CardHead and CardBody should wrap these components:
const CardHead = props => <div>
Head
{props.children}
</div>
...
<Card>
<CardHead><SomeComponent/></CardHead>
</Card>

Related

getBoundingClientRect() on two React components and check if they overlap onScroll

I want to get a ref, more specifically a getBoundingClientRect() on the <Header/> and <Testimonials/> component. I then want to watch for a scroll event and check if the two components ever overlap. Currently, my overlap variable never flips to true even if what appears on the page is that the two components are overlaping.
const [isIntersecting, setIsIntersecting] = useState(false)
const header = useRef(null)
const testimonials = useRef(null)
const scrollHandler = _ => {
let headerRect = header.current.getBoundingClientRect();
let testiRect = testimonials.current.getBoundingClientRect();
let overlap = !(headerRect.right < testiRect.left ||
headerRect.left > testiRect.right ||
headerRect.bottom < testiRect.top ||
headerRect.top > testiRect.bottom)
console.log(overlap) // never flips to true
};
useEffect(() => {
window.addEventListener("scroll", scrollHandler, true);
return () => {
window.removeEventListener("scroll", scrollHandler, true);
};
}, []);
const App = () => {
return (
<div className="App">
<Header />
<LandingPage />
<div style={{ height: '100vh', backgroundColor: 'black', color: 'white' }}>
</div>
<AboutPage />
<TestimonialsPage />
<Footer />
</div>
);
}
First: Components can't receive directly a ref prop, unless you are wrapping the Component itself in a React.forwardRef wrapper:
const Component = React.forwardRef((props, ref) => (
<button ref={ref}>
{props.children}
</button>
));
// Inside your Parent Component:
const ref = useRef();
<Component ref={ref}>Click me!</Component>;
Second: you can also pass a ref down to a child as a standard prop, but you can't call that prop ref since that's a special reserved word just like the key prop:
const Component= (props) => (
<button ref={props.myRef}>
{props.children}
</button>
);
// Inside your Parent Component
const ref = useRef();
<Component myRef={ref}>Click me!</Component>;
This works perfectly fine, and if it's a your personal project you
might work like this with no issues, the only downside is that you
have to use custom prop name for those refs, so the code gets harder to
read and to mantain, especially if it's a shared repo.
Third: Now that you learnt how to gain access to the DOM node of a child Component from its parent, you must know that even if usually it's safe to perform manipulations on those nodes inside a useEffect ( or a componentDidMount ) since they are executed once the DOM has rendered, to be 100% sure you will have access to the right DOM node it's always better using a callback as a ref like this:
const handleRef = (node) => {
if (node) //do something with node
};
<Component ref={handleRef}/>
Basically your function hanldeRef will be called by React during
DOM node render by passing the node itself as its first parameter,
this way you can perform a safe check on the node, and be sure it's
100% valorized when you are going to perform your DOM manipulation.
Concerning your specific question about how to access the getBoundingClientRect of a child Component DOM node, I made a working example with both the approaches:
https://stackblitz.com/edit/react-pqujuz
You'll need to define each of your components as Forwarding Refs, eg
const Header = forwardRef<HTMLElement>((_, ref) => (
<header ref={ref}>
<h1>I am the header</h1>
</header>
));
You can then pass a HTMLElement ref to your components to refer to later
const headerRef = useRef<HTMLElement>(null);
const scrollHandler = () => {
console.log("header position", headerRef.current?.getBoundingClientRect());
};
useEffect(() => {
window.addEventListener("scroll", scrollHandler);
return () => {
window.removeEventListener("scroll", scrollHandler);
};
}, []);
return (
<Header ref={headerRef} />
);
I'm using TypeScript examples since it's easier to translate back down to JS than it is to go up to TS

How to get element height and width from ReactNode?

I have a dynamic component in which I pass in children as prop.
So the props look something like:
interface Props {
...some props
children: React.ReactNode
}
export default Layout({...some props, children}: Props) {...}
I need to access the size of the children elements (height and width), in the Layout component. Note that the children are from completely different components and are non-related.
I can use the Layout component as follow:
<Layout ...some props>
<Child1 /> // I need to know the height and width of this child
<Child2 /> // as well as this child
<Child3 /> // and this child.
</Layout>
How can I do so dynamically? Do I somehow have to convert ReactNode to HTMLDivElement? Note that there is no way I can pass in an array of refs as a prop into Layout. Because that the pages which use Layout are dynamically generated.
Since many doesn't really understand what I meant by dynamically generated. It means that the pages which are using the Layout component can pass in x amount of children. The amount of children is unknown but never 0.
You can achieve this by using React.Children to dynamically build up a list of references before rendering the children. If you have access to the children element references, you can follow the below approach. If you don't then you can follow the bit at the bottom.
You have access to the children element references
If the children components pass up their element reference, you can use React.Children to loop through each child and get each element reference. Then use this to perform calculations before the children components are rendered.
i.e. This is a very simple example on how to retrieve the references and use them.
interface LayoutWrapperProps {
onMount: () => void;
}
const LayoutWrapper: React.FC<LayoutWrapperProps> = ({ onMount, children }) => {
React.useEffect(() => {
onMount();
}, [onMount]);
return <>{children}</>;
};
const Layout: React.FC = ({ children }) => {
const references = React.useRef<HTMLElement[]>([]);
React.useEffect(() => {
references.current = [];
});
function getReference(ref: HTMLElement) {
references.current = references.current.filter(Boolean).concat(ref);
}
function getHeights() {
const heights = references.current.map((ref) =>
ref?.getBoundingClientRect()
);
console.log(heights);
}
const clonedChildren = React.Children.map(children, (child) => {
return React.cloneElement(child as any, {
ref: getReference
});
});
return <LayoutWrapper onMount={getHeights}>{clonedChildren}</LayoutWrapper>;
};
If you don't have access to the children element references
If the children components aren't passing up an element as the reference, you'll have to wrap the dynamic children components in a component so we can get an element reference. i.e.
const WrappedComponent = React.forwardRef((props, ref) => {
return (
<div ref={ref}>
{props.children}
</div>
)
});
When rendering the children components, then the code above that gets the references will work:
<Layout>
<WrappedComponent>
<Child1 />
</WrappedComponent>
</Layout>
Since we don't know how your children is built, here is what I can propose you :
import React from 'react';
import { render } from 'react-dom';
const App = () => {
const el1Ref = React.useRef();
const el2Ref = React.useRef();
const [childrenValues, setChildrenValues] = React.useState([]);
React.useEffect(() => {
setChildrenValues([
el1Ref.current.getBoundingClientRect(),
el2Ref.current.getBoundingClientRect()
]);
}, []);
return (
<Parent childrenVals={childrenValues}>
<span ref={el1Ref}>
<Child value="Hello" />
</span>
<span ref={el2Ref}>
<Child value="<div>Hello<br />World</div>" />
</span>
</Parent>
);
};
const Parent = ({ children, childrenVals }) => {
React.useEffect(() => {
console.log('children values from parent = ', childrenVals);
});
return <>{children}</>;
};
const Child = ({ value }) => {
return <div dangerouslySetInnerHTML={{ __html: value }} />;
};
render(<App />, document.getElementById('root'));
And here is the repro on Stackblitz.
The idea is to manipulate how your children is built.

What is the React way of inserting an icon into another component?

I'm trying to create an WithIcon wrapper component which would insert a child (icon) into a wrapped component.
Let's say I have a button:
<Button>Add item</Button>
I want to create a component WithIcon which will be used like this:
<WithIcon i="plus"><Button>Add item</Button></WithIcon>
Ultimately what I want to achieve is this:
<Button className="with-icon"><i className="me-2 bi bi-{icon}"></i>Add item</Button>
Notice the added className and the tag within the Button's body.
I'm trying to figure out how the WithIcon component's code should look like. What is the React way of achieving this result?
The hardest part was the rules of using the WithIcon Will we only have one ?
Will we have only it at the leftmost ? Something like that.
But if we follow your example. We can relatively write something like this for the WithIcon
const WithIcon = ({ i, children }) => {
return React.Children.map(children, (child) => {
return (
<>
<i className={`me-2 bi bi-${i}`}></i>
{React.cloneElement(child, { className: "with-icon" })}
</>
);
});
};
Then we can just use it the way you want it
<WithIcon i="plus"><Button>Add item</Button></WithIcon>
What we do is just looping through the children which in react is any nested jsx you throw in it (Button in our case)
You can find my fiddle here : https://codesandbox.io/s/react-font-awesome-forked-321tz?file=/src/index.js
UPDATE
So my previous answer does not fully meet the end result we want. The will need to be the main parent
The idea is still quite the same as before but here we are infering the type of the component we passed inside the WithIcon This also adds a safeguard when we passed a nested component inside the WithIcon
const WithIcon = ({ i, children }) => {
return React.Children.map(children, (child) => {
const MyType = child.type; // So we can get the Button
return (
<MyType className="with-icon">
<i className={`me-2 bi bi-${i}`}></i>
{(React.cloneElement(child, {}), [child.props.children])}
</MyType>
);
});
};
I think I'll go to sleep I'll update the rest of the explanation at later date.
See the fiddle here :
https://codesandbox.io/s/react-font-awesome-forked-y43fx?file=/src/components/WithIcon.js
Note that this code does not preserved the other props of the passed component, but you can relatively add that by adding {...child.props} at the MyComponent which is just (reflection like?) of infering the component.
Of course also have another option like HOC Enhancers to do this but that adds a bit of complexity to your how to declare your component api. So Pick whats best for ya buddy
Maybe try using a higher order component?
const withIcon = (icon, Component) => ({children, ...props}) => {
return (
<Component className="with-icon" {...props}>
<i className=`me-2 bi bi-${icon}` />
{children}
</Component>
);
}
Then the usage is
const ButtonWithIcon = withIcon("your-icon", Button);
<ButtonWithIcon>Add Item</ButtonWithIcon>
From my experience with react it usually comes down to either using a property inside the component like here (https://material-ui.com/api/button/) or higher order component like what I described.
There are two common patterns used in React for achieving this kind of composition:
Higher-Order Components
Start by defining a component for your button:
const Button = ({ className, children }) => (
<button className={className}>{children}</button>
);
Then the higher-order component can be implemented like this:
const withIcon = (Component) => ({ i, className = '', children, ...props }) => (
<Component {...props} className={`${className} with-icon`}>
<i className={`me-2 bi bi-${i}`} />
{children}
</Component>
);
Usage:
const ButtonWithIcon = withIcon(Button);
<ButtonWithIcon i="plus">Add Item</ButtonWithIcon>
Context
Start by defining the context provider for the icon:
import { createContext } from 'react';
const Icon = createContext('');
const IconProvider = ({ i, children }) => (
<Icon.Provider value={i}>{children}</Icon.Provider>
);
and then your component:
import { useContext } from 'react';
const Button = ({ className = '', children }) => {
const i = useContext(Icon);
if (i) {
className += ' with-icon';
children = (
<>
<i className={`me-2 bi bi-${i}`} />
{children}
</>
);
}
return (
<button className={className}>{children}</button>
);
};
Usage:
<IconProvider i="plus"><Button>Add Item</Button></IconProvider>

How to create html element dynamically with props based on `as` on react (TypeScript)

I want to create a, b, div or another element based on as={""} prop in my function.
I am checking as and then use if guards to create element one-by-one. But is there any way to create html element based on as component?
I want to be able to create b, a, h4 h3 or any element without if's
I've found similar question but it uses styled components. Is it possible to do it without it?
Currently im doing this:
import * as React from 'react'
interface Props {
children: (React.ReactChild | React.ReactNode) | (React.ReactChild[] | React.ReactNode[])
onClick?: (e) => void
as?: string
}
const DynElement: React.FunctionComponent<Props> = props => {
const { as , children } = props
return (
<>
{as === 'div' && <div {...props_rest}>
{children}
</div>}
{as === 'b' && <b {...props_rest}>
{children}
</b>}
</>
)
}
export default DynElement
and usage is:
<DynElement as="b" onClick={...}>...</DynElement>
Using TypeScript + React.
Capture the as prop in an uppercase variable and use that as a JSX component:
const { as: Cmp = ‘div’, ...rest } = props;
return (
<Cmp {...rest} />
);
No need to even pass children explicitly. It will be passed along with the ‘rest’ props.

Destructuring props in pure function components

Suppose we have the following pure function:
const HeaderComponent = () => (
<h1> Title <h1>
)
export default HeaderComponent
Now I need to receive the new title from props, so we often destructure this.props to avoid accessing title with this.props.title and in the render method we will have something like:
render() {
const {Title} = this.props;
return(
<h1> {Title} </h1>
)
}
The thing is we have to extend React.Component class to access render method.
Is it possible to use the destructure in pure functions?
const HeaderComponent = props => (
// const {Title} = this.props;
)
You can do it like this. I also find it a nice way of making the function self documenting.
const HeaderComponent = ({ title }) => (
<h1>{ title }<h1>
)
Also can set default values
const HeaderComponent = ({ title: 'Default Title' }) => (
<h1>{ title }<h1>
)
UPDATE:
As T.J. Crowder points out, Title is capitalized in your examples above. In the text portion it is lowercase; as that is the norm, I have used the lowercase version
For your specific situation, see ken4z's answer, as parameter destructuring is the most concise way to do that.
But in the general case: If you have logic you need to put in the arrow function prior to the return, just use the verbose form of arrow function:
const HeaderComponent = props => {
const {Title} = props;
// ....more logic can go here...
return <h1>{Title}<h1>;
};
But again, you don't need that just to grab Title from props.
(Side note: It's unusual to capitalize the T in title when it's a property name or variable name...)

Categories

Resources