When trying to pass a component as a prop of another component, everything works fine.
But if i want instead pass a Component and handle its css classes inside the children, I'm currently lost.
In my mind im trying to achieve something similar to this:
import Navbar from 'what/ever/path/Navbar/is/in/Navbar.js';
export default function ParentComponent {
return(
<Navbar NavIcon={<MyIcon/>} />
)
}
.... Imports etc...
export default function Navbar(props) {
const {NavIcon} = props;
return(
<Navigation>
// Now use the Prop as a Component and pass default classNames to it.
// So that we don't need to wrap everything inside a span / div etc.
<NavIcon className="AddCustomStylesAlwaysHere" />
</Navigation>
)
}
Two approaches come to my mind:
Passing a component
Just pass the component and let the parent take care of its instantiation. This way, the only changes you need is making sure <MyIcon /> accepts a className prop:
const MyIcon = ({ className }) => {
return <div className={className} />
};
const Navbar = ({ NavIcon }) => {
return (
<Navigation>
<NavIcon className="AddCustomStylesAlwaysHere" />
</Navigation>
);
};
<Navbar NavIcon={MyIcon} />
Passing an element instance
This way, you take care of instantiating the component and the parent just renders it. In this case, you have to use React utilities to modify existing elements (https://reactjs.org/docs/react-api.html#cloneelement):
const MyIcon = ({ className }) => {
return <div className={className} />
};
const Navbar = ({ NavIcon }) => {
return (
<Navigation>
{React.cloneElement(NavIcon, { className: 'AddCustomStylesAlwaysHere' })}
</Navigation>
);
};
<Navbar NavIcon={<MyIcon />} />
You can use React.Children.map in combination with React.cloneElement:
{
React.Children.map(children, ( child, idx ) => {
return React.cloneElement(child, { className: 'additional-classnames' })
})
}
Related
How to make an intelligent prop type? I have Alert component which has some actions. Those actions can be clickable, there are some different components like Button or Link.
I would like to achieve something like this:
<Alert actions={[{ component: Link, props: { /* here only Link props available */ } }]} />
and
<Alert actions={[{ component: Button, props: { /* here only Button props available */ } }]} />
So props property should determine its type based on component property. Is this possible? I do not want to add any additional generics like
<Alert<ButtonProps> ... />
it should be "intelligent" and do it automatically
You can do this by generics, but it can get a little bit complicated: you need to explicit start which components are to be accepted by <Alert> via its prop type:
interface AlertAction<TFunctionalComponent extends FC<any>> {
component: TFunctionalComponent;
props: ComponentPropsWithoutRef<TFunctionalComponent>;
}
interface Props {
actions: Array<AlertAction<typeof Link | typeof Button>>;
}
export const Alert: FC<Props> = ({ actions }) => {
// Alert component body here
};
However I do see this as an anti-pattern: instead of splitting the component name and props into two separate keys, what if you simply let actions accept an array of ReactElement? i.e.:
interface Props {
actions: ReactElement[];
}
const Alert: FC<Props> = ({ actions }) => {
return <div>
{actions.map(action => <>{action}</>)}
</div>;
};
const MyApp: FC = () => {
return (
<>
{/* Will work */}
<Alert actions={[<Link {...linkProps} />]} />
<Alert actions={[<Button {...buttonProps} />]} />
</>
);
};
If you need to update the props or inject some custom child node into these elements, then you can take advantage of React.cloneChildren:
const Alert: FC<Props> = ({ actions }) => {
return (
<div>
{actions.map((action) => (
<>
{cloneElement(action, {
children: <>Custom child node for action elements</>,
})}
</>
))}
</div>
);
};
Apologies if this has been answered elsewhere, it seems like a basic problem but I wasn't able to find an answer anywhere.
I am trying to find a way to conditionally pass props to children components based on the the type (i.e. component type) of the child.
For instance given a generic functional component
const Parent = ({propA, propB, children, ...props}) => (
<div {...props}>
{children}
</div>
)
in which I expect to receive only A and B components as children. I want propA to be passed only to children of type A and propB to be passed only to children of type B such that
const Parent = ({propA, propB, children, ...props}) => (
<div {...props}>
{React.Children.map<React.ReactNode, React.ReactNode>(children, child => {
if (React.isValidElement(child)) {
if (/* child if of type A */)
return React.cloneElement(child, { propA })
if (/* child if of type B */)
return React.cloneElement(child, { propB })
}
})}
</div>
You can use child.type
const Parent = ({propA, propB, children, ...props}) => (
<div {...props}>
{React.Children.map<React.ReactNode, React.ReactNode>(children, child => {
if (React.isValidElement(child)) {
if (child.type===A) {
return React.cloneElement(child, { propA })
} else {
return React.cloneElement(child, { propB })
}
}
})}
</div>)
if you know the mapping between Component and its respective props, you can create an array of objects and then map over it, while passing it from the parent.
Example
//calling of parent
<Parent renderObject = {[
{prop : propA, comp: compA},
{prop : propB, comp: compB}
]}
...restOfTheProps
/>
Now you can actually code something like this using map
const Parent = ({ renderObject, ...props}) => (
<div {...props}>
{renderObject.map((renderObj)=>{
return <renderObj.comp {...renderObj.prop} />
})}
</div>
)
instead of using children, you can pass component as props.
same this you can do it!
<Modal isActive={true} >
<h1>Modal here!</h1>
</Modal>
and in Your component do same this:
type Props ={
children: React.ReactNode;
isActive: boolean;
}
const Modal:React.FC<Props> = ({children, isActive}) => {
return isActive && children;
}
export default Modal;
I am a beginner in React and what I am doing might not make sense.
What I am trying to do is just to pass a data to from a parent component which is used in every screen to children.
My code is like this.
AppDrawer.tsx
const AppDrawer: React: FC<Props> = ({
children,
}) => {
const [aString, setString] = React.useState<string>('Hello');
...
<div>
<Drawer>
...
</Drawer/>
<main>
<div>
<Container>
{children}
</Container>
</div>
</main>
</div>
App.tsx
<Swith>
<AppDrawer>
<Route path="/" component={ChildCompoent} />
...
...
</AppDrawer>
</Switch>
ChildComponent.tsx
export default class ChildComponent extends React.Component<Props, State> {
state = {
..
}
}
And now I want to access aString in AppDrawer.tsx in child components but I couldn't figure out how I can do it. How can I do it?
I think you can use Context here. Context allows you to pass down something from the parent component, and you can get it at the child component (no matter how deep it is) if you'd like.
I made an example link
You can read more about it here
Updated: I notice you use Route, Router, and I don't use in my codesandbox. However, it's fine though. The main idea is to use context :D
Use render in component Route
<Route
path='/'
render={(props) => (
<ChildCompoent {...props}/>
)}
/>
And should not props aString in component AppDrawer
I'm not sure about the version of react and if it is still supported but this how I did this in my app.
try checking for React. Children over here: https://reactjs.org/docs/react-api.html#reactchildren
see if it's suitable for your case.
render() {
const children = React.Children.map(this.props.children, child => {
return React.cloneElement(child, {
...child.props,
setHasChangesBeenMade: (nextValue) => this.setState({ hasChangesBeenMade: nextValue })
});
});
return (children[0]);
}
for you it should be something like this :
const AppDrawer: React: FC<Props> = ({
children,
}) => {
const [aString, setString] = React.useState<string>('Hello');
...
const childrens = React.Children.map(children, child => {
return React.cloneElement(child, {
...child.props,
aString: aString
});
});
<div>
<Drawer>
...
</Drawer/>
<main>
<div>
<Container>
{childrens[0]}
</Container>
</div>
</main>
</div>
I have a React Wrapper Component, that accepts some props, but forwards all others to the child component (especially relevent for native props like className, id, etc.).
Typescript complains, however, when I pass native props. See error message:
TS2339: Property 'className' does not exist on type
'IntrinsicAttributes & IntrinsicClassAttributes< Wrapper > & Readonly< {
children?: ReactNode; }> & Readonly< WrapperProps>'.
How can I get a component with specific props that also accepts native props (without accepting any props and giving up on type checking)?
My code looks like this:
interface WrapperProps extends JSX.IntrinsicAttributes {
callback?: Function
}
export class Wrapper extends React.Component<WrapperProps>{
render() {
const { callback, children, ...rest } = this.props;
return <div {...rest}>
{children}
</div>;
}
}
export const Test = () => {
return <Wrapper className="test">Hi there</Wrapper>
}
FYI: I found a similar question here, but the answer basically gives up type checking, which I want to avoid: Link to SO-Question
We can have a look at how div props are defined:
interface IntrinsicElements {
div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
}
If we use React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> as the base type we will have all properties of div. Since DetailedHTMLProps just adds ref to React.HTMLAttributes<HTMLDivElement> we can use just this as the base interface to get all div properties:
interface WrapperProps extends React.HTMLAttributes<HTMLDivElement> {
callback?: Function
}
export class Wrapper extends React.Component<WrapperProps>{
render() {
const { callback, children, ...rest } = this.props;
return <div {...rest}>
{children}
</div>;
}
}
export const Test = () => {
return <Wrapper className="test">Hi there</Wrapper> // works now
}
JSX.IntrinsicElements has this info, e.g.
const FooButton: React.FC<JSX.IntrinsicElements['button']> = props => (
<button {...props} className={`foo ${props.className}`} />
)
// alternative...
const FooButton: React.FC<React.PropsWithoutRef<
JSX.IntrinsicElements['button']
>> = props => <button {...props} className={`foo ${props.className}`} />
discovered this in the react-typescript-cheatsheet project.
Have a look at ComponentProps, ComponentPropsWithRef, and ComponentPropsWithoutRef - this will accept a generic input that can be "div", "button", or any other component. It will include react specific props such as className as well:
import React, {
forwardRef,
ComponentPropsWithoutRef,
ComponentProps,
ComponentPropsWithRef
} from "react";
const ExampleDivComponent = forwardRef<
HTMLDivElement,
ComponentPropsWithoutRef<"div">
>(({ children, ...props }, ref) => {
return (
<div {...props} ref={ref}>
{children}
</div>
);
});
<ExampleDivComponent
className=""
style={{ background: "green" }}
tabIndex={0}
onTouchStart={() => alert("touched")}
/>;
const ExampleButtonComponent: React.FC<ComponentProps<"button">> = ({
children,
...props
}) => {
return <button {...props}>{children}</button>;
};
<ExampleButtonComponent onClick={() => alert("clicked")} />;
A co-worker of mine figured it out. Sharing here for broader visibility:
interface ComponentPropTypes = {
elementName?: keyof JSX.IntrinsicElements; // list of all native DOM components
...
}
// Function component
function Component({
elementName: Component = 'div',
...rest,
// React.HTMLAttributes<HTMLOrSVGElement>) provides all possible native DOM attributes
}: ComponentPropTypes & React.HTMLAttributes<HTMLOrSVGElement>)): JSX.Element {
return <Component {...rest} />;
}
// Class component
class Component extends React.Component<ComponentPropTypes & React.HTMLAttributes<HTMLOrSVGElement>> {
render() {
const {
elementName: Component,
...rest,
} = this.props;
return <Component {...rest} />
}
}
I have a component that will sometimes need to be rendered as an <anchor> and other times as a <div>. The prop I read to determine this, is this.props.url.
If it exists, I need to render the component wrapped in an <a href={this.props.url}>. Otherwise it just gets rendered as a <div/>.
Possible?
This is what I'm doing right now, but feel it could be simplified:
if (this.props.link) {
return (
<a href={this.props.link}>
<i>
{this.props.count}
</i>
</a>
);
}
return (
<i className={styles.Icon}>
{this.props.count}
</i>
);
UPDATE:
Here is the final lockup. Thanks for the tip, #Sulthan!
import React, { Component, PropTypes } from 'react';
import classNames from 'classnames';
export default class CommentCount extends Component {
static propTypes = {
count: PropTypes.number.isRequired,
link: PropTypes.string,
className: PropTypes.string
}
render() {
const styles = require('./CommentCount.css');
const {link, className, count} = this.props;
const iconClasses = classNames({
[styles.Icon]: true,
[className]: !link && className
});
const Icon = (
<i className={iconClasses}>
{count}
</i>
);
if (link) {
const baseClasses = classNames({
[styles.Base]: true,
[className]: className
});
return (
<a href={link} className={baseClasses}>
{Icon}
</a>
);
}
return Icon;
}
}
Just use a variable.
var component = (
<i className={styles.Icon}>
{this.props.count}
</i>
);
if (this.props.link) {
return (
<a href={this.props.link} className={baseClasses}>
{component}
</a>
);
}
return component;
or, you can use a helper function to render the contents. JSX is code like any other. If you want to reduce duplications, use functions and variables.
Create a HOC (higher-order component) for wrapping your element:
const WithLink = ({ link, className, children }) => (link ?
<a href={link} className={className}>
{children}
</a>
: children
);
return (
<WithLink link={this.props.link} className={baseClasses}>
<i className={styles.Icon}>
{this.props.count}
</i>
</WithLink>
);
Here's an example of a helpful component I've seen used before (not sure who to accredit it to). It's arguably more declarative:
const ConditionalWrap = ({ condition, wrap, children }) => (
condition ? wrap(children) : children
);
Use case:
// MaybeModal will render its children within a modal (or not)
// depending on whether "isModal" is truthy
const MaybeModal = ({ children, isModal }) => {
return (
<ConditionalWrap
condition={isModal}
wrap={(wrappedChildren) => <Modal>{wrappedChildren}</Modal>}
>
{children}
</ConditionalWrap>
);
}
There's another way
you could use a reference variable
let Wrapper = React.Fragment //fallback in case you dont want to wrap your components
if(someCondition) {
Wrapper = ParentComponent
}
return (
<Wrapper parentProps={parentProps}>
<Child></Child>
</Wrapper>
)
const ConditionalWrapper = ({ condition, wrapper, children }) =>
condition ? wrapper(children) : children;
The component you wanna wrap as
<ConditionalWrapper
condition={link}
wrapper={children => <a href={link}>{children}</a>}>
<h2>{brand}</h2>
</ConditionalWrapper>
Maybe this article can help you more
https://blog.hackages.io/conditionally-wrap-an-element-in-react-a8b9a47fab2
You could also use a util function like this:
const wrapIf = (conditions, content, wrapper) => conditions
? React.cloneElement(wrapper, {}, content)
: content;
You should use a JSX if-else as described here. Something like this should work.
App = React.creatClass({
render() {
var myComponent;
if(typeof(this.props.url) != 'undefined') {
myComponent = <myLink url=this.props.url>;
}
else {
myComponent = <myDiv>;
}
return (
<div>
{myComponent}
</div>
)
}
});
Using react and Typescript
let Wrapper = ({ children }: { children: ReactNode }) => <>{children} </>
if (this.props.link) {
Wrapper = ({ children }: { children: ReactNode }) => <Link to={this.props.link}>{children} </Link>
}
return (
<Wrapper>
<i>
{this.props.count}
</i>
</Wrapper>
)
A functional component which renders 2 components, one is wrapped and the other isn't.
Method 1:
// The interesting part:
const WrapIf = ({ condition, With, children, ...rest }) =>
condition
? <With {...rest}>{children}</With>
: children
const Wrapper = ({children, ...rest}) => <h1 {...rest}>{children}</h1>
// demo app: with & without a wrapper
const App = () => [
<WrapIf condition={true} With={Wrapper} style={{color:"red"}}>
foo
</WrapIf>
,
<WrapIf condition={false} With={Wrapper}>
bar
</WrapIf>
]
ReactDOM.render(<App/>, document.body)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
This can also be used like this:
<WrapIf condition={true} With={"h1"}>
Method 2:
// The interesting part:
const Wrapper = ({ condition, children, ...props }) => condition
? <h1 {...props}>{children}</h1>
: <React.Fragment>{children}</React.Fragment>;
// stackoverflow prevents using <></>
// demo app: with & without a wrapper
const App = () => [
<Wrapper condition={true} style={{color:"red"}}>
foo
</Wrapper>
,
<Wrapper condition={false}>
bar
</Wrapper>
]
ReactDOM.render(<App/>, document.body)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
With provided solutions there is a problem with performance:
https://medium.com/#cowi4030/optimizing-conditional-rendering-in-react-3fee6b197a20
React will unmount <Icon> component on the next render.
Icon exist twice in different order in JSX and React will unmount it if you change props.link on next render. In this case <Icon> its not a heavy component and its acceptable but if you are looking for an other solutions:
https://codesandbox.io/s/82jo98o708?file=/src/index.js
https://thoughtspile.github.io/2018/12/02/react-keep-mounted/