React Response: Render Props

Series Intro feel free to skip

Partially due to the fact that I find coming up with things to write about somewhat difficult, I've been seeking articles from the React community to rewrite for Ember (primarily on twitter). This'll help the searchability of common patterns people may be familiar with when coming from React, or any other ecosystem which also has a similar nomenclature.

My hope for this series is only two things:

  • Improve the perception of Ember with respect to modern features and behavior
  • Show how conventions and architectural patterns can make everyone's lives easier.

Rather than a response to a particular blog featuring React, this is more of a demonstration of correlating design patterns between the two ecosystems. Thanks to @vlascik for the suggestion to cover React's render props pattern.

First, let's clarify what a render prop is for those who may not be familiar with the term. According to the React Documentation:

The term “render prop” refers to a technique for sharing code between React components using a prop whose value is a function.

In React, components are just functions™, so any prop passed to a component whos value is a function that returns JSX is considered a "render prop".

Some examples:

<Header
  {/* profileImage is the render prop */}
  profileImage={profileProps => {
    return (
      <LargeProfileImage {...profileProps} />
    )
  }
}>
  <AppNavigation />
</Header>
<Header>
  <AppNavigation />
  {/* children, an implicit render prop passed to Header when there is block content */}
  <LargeProfileImage />
</Header>

where Header could be defined as:

export function Header({ profileImage, children }) {
  return (
    <header>
      <HomeLink />

      {children}

      {profileImage &&
        profileImage({ className: 'header-image' })
      }
    </header>
  )
}

When would someone want to use render props in React?

When a part of a component's template needs to be different between usages of that component. Rather than using conditionals in the component (in this case Header), render props allow yielding control to the calling context.

Ember has the exact same capabilities, but under a different name: "Yieldable Named Blocks". Which we'll get to after we talk about the default / easy thing to do with both ecosystems.

{children}

For {children}, however, there is an exact corollary in Ember: {{yield}}.

Both words have great semantic meaning as well.

A parent component yields the rendering context to its children.

The default template generated for a component contains a single line: {{yield}}. Let's change that to mimic the React example:

<header>
  <HomeLink />

  {{yield}}
</header>

aside from not having the profileImage render prop, these templates are the exact same, but with {children} replaced with {{yield}}

Any other render prop

In Ember, we'd still use yield, but with a parameter to create named 'blocks' that the calling context can use. These yields with the to argument are "Yieldable Named Blocks" (available in Ember 3.20+)

Example using Header, from above:

<header>
  <HomeLink />

  {{yield}}

  {{yield to='image'}}
</header>

and then the calling context would look like:

<Header>
  <AppNavigation />

  <:image>
    <LargeProfileImage />
  </:image>
</Header>

Passing Arguments

For both of these examples, assume there is a UI Library which provides a bunch of component primivites for constructing the common UI pattern / "Card".

in React:

export function SomeComponent({ header, content, children, footer }) {
  return (
    <Card>
      {header && (
        <CardHeader>
          {header({
            Image: CardHeaderImage,
            Icon: CardHeaderIcon
          })}
      </CardHeader>
      )}

      <CardContent>
        {children}
        {content && content()}
      </CardContent>

      {footer && (
        <CardFooter>
          {footer()}
        </CardFooter>
      )}
    </Card>
  )
}

and then in the calling context

export function App() {
  return (
    <SomeComponent
      header={({ Image }) => {
        return (
          <>
            <Image src='path/to-image.png' />

            My Header!
          </>
        );
      }}

      footer={() => {
        return (
          <button>Call to Action</button>
        );
      }}
    >
      freely yielded content
    </SomeComponent>
  );
}

in Ember:

{{!-- some-component --}}
<Card>
  {{#if (has-block 'header')}}
    <CardHeader>
      {{yield
        hash=(
          image=(component 'card-header-image')
          icon=(component 'card-header-icon')
        )
        to='header'
      }}
    </CardHeader>
  {{/if}}

  <CardContent>
    {{yield}}
    {{yield to='content'}}
  </CardContent>

  {{#if (has-block 'footer')}}
    <CardFooter>
      {{yield to='footer'}}
    </CardFooter>
  {{/if}}
</CardModal>
{{!-- the calling context --}}
<SomeComponent>
  <:header as |headerComponents|>
    <headerComponents.image src='path/to-image.png' />

    My Header!
  </:header>

  <:footer>
    <button>Call to Action</button>
  </:footer>

  <:default>
    freely yielded content
  </:default>
</SomeComponent>

What's cool about named blocks is that, as a component author, you are in control of the order the components are rendered. So in the above example where header is first and footer is second, those could be flipped, but still rendered in the same locations, because the content is placed where the corresponding {{yield to="name"}} block is. I like that a lot, and it's something I always wanted when I was writing React components.

Want More Information?