Brandon Durham
Brandon Durham

Reputation: 7727

How do I conditionally wrap a React component?

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

Answers (13)

Max
Max

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

vsync
vsync

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

Sid
Sid

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

Franck Wolff
Franck Wolff

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

antony
antony

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

Mustkeem K
Mustkeem K

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

sytolk
sytolk

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

Adam Ch.
Adam Ch.

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

Avinash
Avinash

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

Tomek
Tomek

Reputation: 568

You could also use a util function like this:

const wrapIf = (conditions, content, wrapper) => conditions
        ? React.cloneElement(wrapper, {}, content)
        : content;

Upvotes: 0

Do Async
Do Async

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

Sulthan
Sulthan

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

hobenkr
hobenkr

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

Related Questions