Components: Blackbox Scheme

The “blackbox” codegen scheme for components aims to strictly separate files that are owned by Plasmic and the files that are owned by developers. The Plasmic-owned files define the presentation, and the developer-owned files define the behavior. Developers use the Plasmic-owned files as a “blackbox” library to help them render components as they are designed in Plasmic.

By our guiding principles, the blackbox scheme allows you to:

  • Update components with new designs by overwriting the Plasmic-owned files every time you run plasmic sync. Since your edits are in developer-owned files, they will be safe, and will continue to work as long as the design didn’t introduce breaking changes.
  • Control component props by giving you total freedom in defining what props your component take, as long as you can map them to props that Plasmic-generated component understands.
  • Instrument components to do anything by providing a rich API for attaching and overriding behavior to the Plasmic-generated elements, and letting you use React hooks as you usually would to manage state and fetch data.

Generated files

For each component in the Plasmic project, we generate two sets of files:

  • Presentational Plasmic* components (e.g. plasmic/PlasmicButton.tsx) — The files in the plasmic directory are a blackbox that know how to render the component. Think of this as a utility library that you can call to render some UI. It is owned by Plasmic, and shouldn’t be edited by you. As you iterate on the design using Plasmic, these files will be updated when you run plasmic sync.
  • Wrapper components (e.g. Button.tsx) — This source file is owned by you, and should be edited by you. Here, you define the actual React component used throughout your application, and make use of the presentational Plasmic* component above to render the component, attaching the appropriate data bindings and event handlers as necessary. Plasmic generates an empty scaffolding for this file the first time this component is synced, and never touches it again.

When you first plasmic sync your Button component, you might see a Button.tsx file that looks something like this:

function Button(props: ButtonProps) {
  return <PlasmicButton {...props} />;
}

Here, the Button component is the actual React component that you would use to create a button throughout the rest of your application. In turn, it calls the presentational PlasmicButton component to do the actual rendering.

We will talk about each of these in turn.

Working with Plasmic* components

The “presentational library” components like PlasmicButton expose a purely presentational component that knows how to render and style a component exactly as designed in Plasmic, and nothing else. These Plasmic* components expose props that allows you to control which variants to activate, which slots to fill with what content, and which elements to instrument with real data or event handlers. These files are owned by Plasmic and shouldn’t be edited by you; when the design for Button gets updated, plasmic sync will overwrite PlasmicButton.tsx to reflect the new designs.

Note that the Plasmic* components should only be used by its corresponding wrapper component (PlasmicButton should only be used by Button). You should always use the wrapper component (Button) from the rest of your application. In this way, the Plasmic* is completely encapsulated by the wrapper component; no one else needs to know about PlasmicButton except for Button.

There are four classes of props that you can pass to the Plasmic* components:

Variant props

Component variants are organized into groups. For example, in a Button component, we may have a variant group role that includes primary and secondary variants, or a size group that includes small and large variants. Often the groups are single-choice — you only allow one variant per group to be active. But sometimes it makes sense for them to be multi-choice — for example, a Button component may have a variant group withIcons that has options prefix and suffix, both of which can be true at the same time.

Each variant group is a prop on the Plasmic* presentational component.

  • For single-choice variant groups, you can pass in the name of the variant you want to activate, or undefined if none should be activated.
  • For multi-choice variant groups, you can pass in an array of variant names, or a object of variant names mapping to true or false.

Example:

// Passing in literals to turn on `role` and `withIcons` variants
<PlasmicButton role="primary"  withIcons={["prefix", "suffix"]} />
// Turning on variants conditionally
<PlasmicButton 
  role={isPrimary() ? "primary" : isSecondary() ? "secondary" : undefined}  withIcons={{    prefix: hasPrefixIcon(),    suffix: hasSuffixIcon()  }}/>

The variants prop

Instead of using the prop for each variant group, you can also use the variants prop, which allows you to specify all variants in one object:

<PlasmicButton
  variants={{    role: isPrimary() ? "primary" : isSecondary() ? "secondary" : undefined,
    withIcons: {
      prefix: hasPrefixIcon(),
      suffix: hasSuffixIcon()
    }
  }}
/>

Slot Props

Plasmic* components can also take in props that correspond to slots defined for the component. For example, a Button component might have slots that correspond to the button text (children ), the prefix icon, and the suffix icon:

<PlasmicButton prefixIcon={...} suffixIcon={...}>  Hello!</PlasmicButton>

Instead of using the prop for each slot, you can also use the args prop, which allows you to specify all such args in one object:

<PlasmicButton
  args={{    prefixIcon: ...,    suffixIcon: ...,    children: "Hello!"  }}/>

Override props

The component as designed in Plasmic creates a tree of elements that corresponds to the tree of layers you see in Plasmic. The override props allow you to customize this tree of elements exactly as you see fit to make your component come alive. You can modify the props used for each element, attach event handlers, override rendered content, wrap elements in other React components, and more. We want you to have total control in making the element tree exactly what you want.

You reference the element you want to override by its name; so if you want to override an element, you must first name that element in Plasmic.

For example, for the Button component, you might want to override its root element to attach click handlers:

<PlasmicButton 
  root={{    props: {      onClick: () => alert("I got clicked!")    }  }}/>

Or maybe you want to render the PlasmicButton as a link instead:

<PlasmicButton 
  root={{
    as: "a",    props: {
      href: "https://plasmic.app"    }
  }}
/>

The object you pass into the named node (“root”) above is the “Override object”. The Override object supports the following properties; you can mix and match them as needed:

props

Object of props to use for that element. Note that the element may be a normal HTML tag — in which case you can pass in HTML attributes — or a component — in which case you can pass in component props. For example,

{
  props: {
    title: user.name,
    onClick: () => ...,
    // etc.
  }
}

as

The React.ElementType to use to render this element. This can be an HTML tag like “a” for links, or a React component. This element will then be rendered with that element type. For example,

{
  as: "a",
  props: {
    href: ...
  }
}

render

A function that takes in the default props, and returns a ReactNode. Doing this will completely replace this element with whatever is returned from the render function; useful if you want to completely swap in different content for this element.

For example,

{
  render: () => <TotallyDifferentComponentPsych />
}

If you can also return null from your render function if you don’t want to render this element at all:

{
  render: () => null
}

If you pass in a render function, then props and as are ignored.

wrap

A function that takes in the rendered ReactNode for this element, and returns another ReactNode. This is useful if you just want to wrap this element in something, like a context provider. For example,

{
  wrap: node => <Context.Provider value={}>{node}</Context.Provider>
}

wrapChildren

A function that takes in the rendered children for this element, and returns another ReactNode. This is useful if you want to append or prepend some custom content as a child of this element. For example, to implement an accessible “checkbox”, you may want to sneak in a visually hidden input element that’s not actually in the design:

{
  // insert a visually hidden checkbox input as the first child
  wrapChildren: children => (
    <>
      <input className="visually-hidden" type="checkbox"/>
      {children}
    </>
  )
}

Override object shorthands

Instead of passing in a full Override object as specified above, you can instead use one of these shorthands:

  • An object of prop overrides — if you pass in an object without any of the known keys above (props, as, render, wrap, and wrapChildren), we interpret that object as just a props override, equivalent to passing in {props: …}.
  • A ReactNode — we interpret this as the children override, equivalent to {props: {children: …}}.
  • A function — we interpret this as the render override, equivalent to {render: …}.

Root override props

Any additional props you pass into a Plasmic* component are interpreted as an override for the root element. For example, instead of using the root prop as we did before, we can directly set the overrides on the Plasmic* component:

<PlasmicButton 
  // This is interpreted as a prop override for the `root` element  onClick={() => alert("I got clicked!")}/>

Working with Wrapper Components

The wrapper components, like Button.tsx, define the actual React component that is used throughout the rest of your application. It can make use of the PlasmicButton component to render the button exactly as it was designed.

There are no restrictions to the edits you make to the wrapper component file. You can completely change the functional component to a class component. You can export exactly the props interface you want. You can add hooks, fetch data, add other components, etc. You can even make it not use the presentational library at all!

The main job of the wrapper component is to:

  1. Install any React hooks for state or data fetching.
  2. Pass in the appropriate variants to activate to the Plasmic* component.
  3. Pass in the appropriate content for each slot to the Plasmic* component.
  4. Attach event handlers and other overrides on the elements in the Plasmic* component.

Your wrapper component file starts out like this:

import * as React from "react";
import { 
  PlasmicButton, 
  DefaultButtonProps 
} from "./plasmic/PlasmicButton";

interface ButtonProps extends DefaultButtonProps {
}

function Button(props: ButtonProps) {
  return <PlasmicButton {...props} />;
}
export default Button;

We’ll dive into the different pieces of this file here!

The wrapper component props

ButtonProps above extends from DefaultButtonProps. This set of default props are generated by Plasmic, and corresponds to the set of component props that Plasmic knows about.

Specifically, you will find a prop there for each:

  • Component variant group
  • Component slot

For the Button component we’ve been working with, you DefaultButtonProps might look something like…

interface DefaultButtonProps {
  // variants
  role?: "primary"|"secondary";
  withIcons?: 
    | "prefix"
    | "suffix"
    | ("prefix"|"suffix")[]
    | {[v in "prefix"|"suffix"]?: boolean};

  // slots
  children?: React.ReactNode;
  prefixIcon?: React.ReactNode;
  suffixIcon?: React.ReactNode;
}

These are the ways in which one can customize a component instance within Plasmic, so we start out assuming that these are also the props that you’d want for your React component.

When the Button component calls <PlasmicButton {…props}/>, then, it is simply instantiating PlasmicButton with the activated variants and slot contents that the Button component itself was instantiated with.

If your component is also a purely presentational component with no additional logic or state, then this might be all you need!

But, sometimes that’s not the case. By our guiding principle, we want you to be able to curate your component props exactly as you’d like,

Adding additional props

You should really be able to pass in onClick handler to the Button component, or it’d be pretty useless! We can do so by modifying the starting code like this:

interface ButtonProps extends DefaultButtonProps {
  onClick?: (e: React.MouseEvent) => void;}

function Button(props: ButtonProps) {
  const { onClick, ...rest } = props;  return (
    <PlasmicButton 
      {...rest}       root={{        onClick: onClick      }}    />
  );
}

Note that because extra props sent to PlasmicButton are interested as root prop overrides, you can also simply do:

<PlasmicButton {...rest} onClick={onClick} />

Or, how about, sometimes, you want to be able to render Button as a link instead, if there’s an href passed in?

interface ButtonProps extends DefaultButtonProps {
  onClick?: (e: React.MouseEvent) => void;
  href?: string;
}

function Button(props: ButtonProps) {
  const { onClick, href, ...rest } = props;  return (
    <PlasmicButton 
      {...rest} 
      root={{
        as: href ? "a" : undefined, // undefined uses default element type        props: {                  onClick: onClick          href: href        }      }}
    />
  );
}

Or, maybe you want to just dump the kitchen sink of all possible “button” props?

interface ButtonProps extends DefaultButtonProps, React.ComponentProps<"button"> {  href?: string;
}

function Button(props: ButtonProps) {
  const {    role, withIcons, children, prefixIcon, suffixIcon, href,     ...buttonProps  } = props;  return (
    <PlasmicButton 
      role={role}      withIcons={withIcons}      children={children}      prefixIcon={prefixIcon}      suffixIcon={suffixIcon}      root={{
        as: href ? "a" : undefined, // undefined uses default element type        props: {                  href: href,          ...buttonProps        }      }}
    />
  );
}

Important note: when you add new props to your wrapper component, it may no longer make sense to spread your wrapper component props to the Plasmic* component, since all extra props are interpreted as override props for your root element! For example,

interface TaskProps extends DefaultTaskProps {
  task: TaskItem;
}

function Task(props: TaskProps) {
  // Carefully pluck out `task` so that we don't pass it along as a prop to
  // `PlasmicTask`, which will interpret it as an override prop for the root
  // element, which may not make sense.
  const {task, ...rest} = props;
  return <PlasmicTask {...rest} children={task.name}/>
}

Removing props from defaults

Sometimes, instead of adding props, you want to remove some of the default props. For example, for the Button example above, you may not like having the withIcons variant group as a Button prop; shouldn’t we be able to just derive what withIcons variants to activate by whether a prefixIcon or a suffixIcon was given?

Indeed we can!

interface ButtonProps extends Omit<DefaultButtonProps, "withIcons"> {  onClick?: (e: React.MouseEvent) => void;}

function Button(props: ButtonProps) {
  const { onClick, ...rest } = props;
  return (
    <PlasmicButton 
      {...rest} 
      withIcons={{        prefix: !!props.prefixIcon,        suffix: !!props.suffixIcon,      }}      root={{
        onClick: onClick
      }}
    />
  );
}

Here, we use the built in Typescript utility type [Omit](https://www.typescriptlang.org/docs/handbook/utility-types.html#omittk) to help us exclude the props from DefaultButtonProps that we don’t need, and instead derive it ourselves.

When you do this, though, you should set the corresponding withIcons prop’s “Export type” to “Internal” in Plasmic, by going to the Props tab of the component panel:

This tells Plasmic that this prop is only used “internally” within the wrapper component — that is, even though PlasmicButton knows about the withIcons prop, the wrapper Button component does not. This means whenever Plasmic generates code that uses Button, it will do so without specifying a withIcons prop.

Another scenario where this is useful is when you have variants that correspond to internal state:

function Task(props: TaskProps) {
  const [editing, setEditing] = useState(false);  return (
    <PlasmicTask
      // Activating the `isEditing` variant is controlled by internal component       // state, not by props.      isEditing={editing ? "isEditing" : undefined}    />
  );
}

To recap, you should remove props from your default wrapper props when they can be derived from other props, state, or context. Again, this is about giving you the control you want to craft your component props in a way that makes sense for your application.

Completely up to you

You can take this even further, and completely abandon the generated default props:

interface ButtonProps {
  // Whatever you want here, don't even extend from `DefaultButtonProps`!
}

This is useful when your wrapper component props really wildly differs from the Plasmic* props. For example, maybe your component doesn’t need any props at all, and fetches data dynamically using a hook, or reads data from a context. Or, maybe your component props are in terms of your domain data model — a `Tweet component for a to-do app might take in a data object as prop, instead of variants like “completed” or slots like “name”:

interface TaskProps {
  // domain data model object
  task: TaskItem;
}

function Task(props: TaskProps) {
  const {task} = props;
  const [isEditing, setEditing] = React.useState(false);
  return (
    <PlasmicTask
      // slots
      children={task.name}

      // variants
      isCompleted={task.completed}
      isEditing={isEditing}

      // overrides
      editButton={{
        onClick: () => setEditing(true)
      }}
      // etc.
    />
  );
}

In these cases, it is up to you to map the wrapper component props to the Plasmic* props, to render the right thing.

Note a trade-off here, however. Plasmic does not know about or understand these domain data models; it only understands the variants and slots defined in Plasmic. In the Task example above, Plasmic doesn’t know about TaskItem, only variants like isCompleted or isEditing. That means when Plasmic generates code that uses your Task component, it will not know how to do it right. Instead, you will need to provide the appropriate overrides to feed your Task component the right props.

For example, consider a PinnedTask component renders the currently-pinned TaskItem:

function PinnedTask(props: PinnedTaskProps) {
  const pinnedTaskItem = usePinnedTaskItem();
  return (
    <PlasmicPinnedTask
      // override the props corresponding to the Task element so it gets the
      // domain data object it expects
      task={{        task: pinnedTaskItem      }}    />
  );
}

Using React hooks

Because your wrapper components are just like normal components, you can just use hooks for state management and data fetching as you normally would. You can already see many examples of using React hooks within wrapper components on this page.

Using class-based components

You can write your wrapper component however you’d like, including making it a class-based component instead!

class Button extends React.Component<ButtonProps> {
  render() {
    return <PlasmicButton ...>;
  }
}

Rendering specific sections or elements

So far we’ve shown how, given a Plasmic component, you can render the entire component. However, you can also pluck out and render any named element in a component.

<PlasmicProfileCard.followButton  onClick={...}
  children={`Follow ${user.name}`}
/>

These are all accessible as components themselves that take all the same parameters (variants, slots, overrides) as in the original component, but filtered to just the elements within that sub-tree.

This is particularly helpful for one common scenario: wanting to turn a static list in a design into a dynamic list, which we’ll explain in the next section.

This may also be (more rarely) useful for complex, highly dynamic components where you just want to grab whatever pieces of the component suit you, and then compose them together.

const variants: PlasmicProfileCard__VariantArgs = {
  state: "following"
};

// all I want is the footer and the followButton...
<div>
  <div className="whatevers">
    <PlasmicProfileCard.footer variants={variants} />  </div>
  <div>          
    // ima gonna just stick this right here
    <PlasmicProfileCard.followButton variants={variants} .../>  </div>
</div>

One thing to keep in mind is that you’ll likely want to render all these parts of the component in the same variant of that component. In the above example, we put the variants into a variable that we then consistently pass to all the parts.

Being able to pluck out pieces of the design and recompose them however you want is a powerful feature that gives you total flexibility in how to render your component. However, we generally encourage trying to stick to rendering the entire component when you can, since rendering the entire component keeps you closest to the original design even as the design evolves.

Repeated elements

Designs in Plasmic are static mocks, including any lists of elements. But in the real app you may want these to be dynamic sets of elements.

For instance, consider a Plasmic todo app, where you have a tasksContainer list that includes four instances of a Task component. Say these instances are elements named task1, task2, etc. This might look as follows:

Of course, this list is just a mock of four examples tasks, and in the real product you’ll want this to be a dynamic list ultimately coming from some data store or application state for the current user. Because these tasks are instances of a Task component, you can simply replace the contents of that taskContainer with your own instances:

<PlasmicTodoApp
  variants={variants}
  tasksContainer={{
    children: tasks.map(task => (
      <Task task={task} />
    ))
  }}
})

But what if taskContainer didn’t simply contain Task instances, but rather just boxes or other non-component elements? Or what if it did contain Task instances, but the designer wants specific variants (or other props/styling) on them? Or what if we want to be robust to changes in the design—perhaps today it’s a collection of plain Task instances, but tomorrow it will be ?

A different approach is that you can render just the taskTemplate element (whatever it is—plain Task instance or not), using the approach described in the previous section for rendering any specific element:

// We want to render both the root element and the taskTemplate elements in the
// "nonempty" variant of the component, so lift the variants out into a reusable
// variable.
const variants: PlasmicTodoApp__VariantsArgs = {
  state: "nonempty"
};

<PlasmicTodoApp
  variants={variants}
  tasksContainer={{
    children: tasks.map(task =>
      // Use the renderer for taskTemplate, overriding what you need to.      // We take care to render it in the right variants.      <PlasmicTodoApp.taskTemplate        variants={variants}        name={task.name}      />    )
  }}
})

This will ensure the tasks you dynamically render have all the other props and styling applied to taskTemplate by the designer in Plasmic Studio.

Substituting code components

Let’s say that you want to create a design that includes instances of components that already exist in code, or that otherwise aren’t going to be Plasmic-generated. For instance, your product codebase already has implemented components for your design system, or you want to drop in a rich text editor like Slate.

In Plasmic, you would just design mocks of these components as a design-time placeholder, and then in the generated component file, you can simply not call into the presentational library at all, but instead render the code component that you wish to use!

For instance, for the Slate rich text editor—in Plasmic you can create a component called SlateShim with a big text area. The generated SlateShim component normally renders the PlasmicSlateShim presentational component. It’s as easy as replacing all that with just rendering the real Slate component:

import Slate from 'slate';
function SlateShim() {
  // Just don't render <PlasmicSlateShim/> at all!
  return <Slate ... />;
}

ProfileCard example

You can check out an example building out a ProfileCard using some of these more advanced, along with how it compares with the Direct Edit Codegen Guide scheme.

The Renderer API

There is an alternative “Renderer API” that you can use to render your Plasmic components. It looks like this:


function Button(props: ButtonProps) {
  const renderer = PlasmicButton.createRenderer();
  return renderer
    .withVariants({
      role: props.role,
      withIcons: {
        prefix: !!props.prefixIcon,
        suffix: !!props.suffixIcon
      }
    })
    .withArgs({
      children: props.children,
      prefixIcon: props.prefixIcon,
      suffixIcon: props.suffixIcon
    })
    .withOverrides({
      root: {
        onClick: props.onClick
      }
    })
    .render();
}

You instantiate a renderer by calling the createRenderer() function on your Plasmic* component. Then, instead of passing in variants / args / overrides as props to your Plasmic* component, you can use the above builder pattern. The final call to render() will output the rendered React element tree.

You can also pluck out just specific parts of the design as well:


const renderer = (
  PlasmicProfileCard.createRenderer()
  .withVariants({
    isFollowing: isFollowing
  })
);

return (
  <div>
    <div className="whatevers">
      {renderer.forNode("footer").withOverrides(...).render()}
    </div>
    <div>          
      {renderer.forNode("followButton").withOverrides(...).render()}
    </div>
  </div>
);

When to use the Renderer API

One small but obvious advantage of the Renderer API is that you only render one React component instead of two; when you render a Button component, you don’t have to also render a PlasmicButton component.

But the main advantage of the Renderer API is that it enables some advanced patterns that allows you to separate out some concerns. For example, you could build a function that tracks clicks on certain elements in a Plasmic component:

function trackClicks(renderer: Renderer, ...elements: string[]) {
  for (const element of elements) {
    renderer = renderer.withOverrides({
      [element]: {
        onClick: () => recordClick(element)
      }
    });
  }
  return renderer;
}

function ProfileCard(props) {
  let renderer = PlasmicProfileCard.createRenderer();
  renderer = trackClicks(renderer, "followButton", "emailButton");
  return renderer.withVariants(...).withArgs(...).withOverrides(...).render();
}

Still, we recommend that you use the Plasmic* components by default unless you really need the advanced use patterns.