Reputation: 7727
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>
);
Upvotes: 160
Views: 89841
Reputation: 1
None of the solutions here were quite satisfactory for what we were looking for in our team.
The following Code Sandbox has three different implementations: https://codesandbox.io/p/sandbox/objective-fermat-52hn4k
The nicest of which is the following. Unfortunately, I couldn't get the typings correctly in that one (so if you have an idea on how to do that, please leave a comment).
MaybeWrapped3
is, in my opinion, also very neat, but the way it is used will likely divide opinions.
import React, { useState } from "react";
type MaybeWrappedProps<P extends React.PropsWithChildren<{}>> = P &
React.PropsWithChildren<{
when: boolean;
Wrap: React.FC<P>;
}>;
function MaybeWrapped<P extends React.PropsWithChildren<{}>>({
when,
Wrap,
children,
...wrapperProps
}: MaybeWrappedProps<P>) {
if (!when) {
return <>{children}</>;
}
return <Wrap {...wrapperProps}>{children}</Wrap>;
}
type MaybeWrappedProps2 = React.PropsWithChildren<{
when: boolean;
wrap: (children: React.ReactNode) => React.ReactElement;
}>;
function MaybeWrapped2({
when,
wrap,
children,
...wrapperProps
}: MaybeWrappedProps2) {
if (!when) {
return <>{children}</>;
}
return wrap(children);
}
type MaybeWrappedProps3<P> = {
when: boolean;
Wrap: React.FC<P>;
};
const MaybeWrapped3 =
<P,>({ when, Wrap }: MaybeWrappedProps3<P>) =>
(wrapperProps: P) =>
({ children }: React.PropsWithChildren<{}>) => {
if (!when) {
return <>{children}</>;
}
return <Wrap {...wrapperProps}>{children}</Wrap>;
};
type WrapperProps = React.PropsWithChildren<{ title: string }>;
const Wrapper: React.FC<WrapperProps> = ({ title, children }: WrapperProps) => {
return (
<>
<h1>{title}</h1>
{children}
</>
);
};
export default function App() {
const [show, setShow] = useState(true);
const MW3 = MaybeWrapped3({ when: show, Wrap: Wrapper })({
title: "Greeting from MaybeWrapped3",
});
return (
<div className="App">
<button onClick={() => setShow((prev) => !prev)}>toggle</button>
{/*
Neatest but I couldn't get the typing to work 100% (and I don't think it will work with class components).
On the other hand, if you don't care about types nor class components, this works great!
*/}
<MaybeWrapped
when={show}
Wrap={Wrapper}
title="Greeting from MaybeWrapped"
>
<p>Hello World!</p>
</MaybeWrapped>
{/* Less neat but types work. */}
<MaybeWrapped2
when={show}
wrap={(children) => (
<Wrapper title="Greeting from MaybeWrapped2">{children}</Wrapper>
)}
>
<p>Hello World!</p>
</MaybeWrapped2>
{/* Only way I could get the types work properly, with usage almost great. */}
<MW3>
<p>Hello World!</p>
</MW3>
</div>
);
}
Upvotes: 0
Reputation: 130580
I've made a robust wrapper component which is very intuitively-named, easy to understand, and also support native HTML tags as wrappers:
<WrapIf>
:const WrapIf = ({ condition, With, children, ...rest }) => {
if (typeof With === 'string') {
return condition
? React.createElement(With, rest, children)
: children
} else {
// 'With' is a React component
return condition ? <With {...rest}>{children}</With> : children;
}
}
const Wrapper = ({children, ...rest}) => <h1 {...rest}>{children}</h1>
// demo app: with & without a wrapper
const App = () => [
// works
<WrapIf condition={true} With={Wrapper} style={{color:"red"}}>
{`Wrapped with <Wrapper>`}
</WrapIf>
,
// will not work because `h1` is not a React component
<WrapIf condition={true} With="h2" style={{color:"blue"}}>
{`wrapped with native HTML <h2>`}
</WrapIf>
,
// will not wrap the children
<WrapIf condition={false} With={Wrapper}>
Wrapper is disabled
</WrapIf>
]
ReactDOM.render(<App/>, document.body)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
Upvotes: 0
Reputation: 1014
This is old, but the answers here will be buggy in 90% of cases because when the parent re-renders the child components will be unmounted / remounted. Heres a component you can use instead:
type ConditionallyWrapProps<T extends React.FC<any>> = {
condition: boolean | undefined;
children: ReactNode;
Wrapper: T;
} & Omit<React.ComponentProps<T>, "condition" | "Wrapper">;
export const ConditionallyWrapV2 = <T extends React.FC<any>>({
condition,
children,
Wrapper,
...wrapperProps
}: ConditionallyWrapProps<T>) => {
return (
<>
{condition ? (
<Wrapper {...(wrapperProps as any)}>{children}</Wrapper>
) : (
children
)}
</>
);
};
Upvotes: 0
Reputation: 96
I'm personally using this kind of code (ComponentChildren is Preact, you should be able to use ReactNode for React):
const Parent = ({ children }: { children: ComponentChildren }) => props.link != null ?
<a href={ props.link }>{ children }</a> :
<>{ children }</>
return (
<Parent>
<i className={ styles.Icon }>
{ props.count }
</i>
</Parent>
)
Upvotes: 0
Reputation: 2893
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>
);
}
Upvotes: 54
Reputation: 8848
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>
)
Upvotes: 0
Reputation: 7383
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/
Upvotes: -1
Reputation: 183
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
Upvotes: 8
Reputation: 2004
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>
)
Upvotes: 26
Reputation: 568
You could also use a util function like this:
const wrapIf = (conditions, content, wrapper) => conditions
? React.cloneElement(wrapper, {}, content)
: content;
Upvotes: 0
Reputation: 4284
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>
);
Upvotes: 53
Reputation: 130172
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.
Upvotes: 152
Reputation: 1234
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>
)
}
});
Upvotes: 0