Using code components in Plasmic Studio

You can use custom React components—from your own codebase or imported from npm libraries—directly as building blocks in Plasmic Studio.

This is a powerful capability that enables teams to:

  • Use their existing design system.
  • Show dynamic data.
  • Use components of arbitrary complexity and behavior.
  • Introduce interactions, effects, and functionality not yet built into Plasmic Studio.

You can seamlessly interleave code components and content you’ve designed in Plasmic Studio, using slots.

See the full code component API reference.

Set up app hosting

Important: do not skip this step.

To use code components in Plasmic Studio, you need to first set up a project to use app hosting.

Register code components from the host application

See the full code component API reference.

Use code components in Studio

Once the components are registered, you will be able to use them in your project—no different from any other component created in Plasmic!

Just like imported components, code components cannot be edited in Plasmic Studio (only instances of it can be customized).

Detecting when the component is rendering in Plasmic Studio

You might want to disable some functionality inside the component when editing it in Plasmic Studio—for example, disable slow scroll animations, videos, or certain compute-heavy effects. For that reason, we provide a React context PlasmicCanvasContext whose value is true only when the component is rendering in the Editor.

Copy
import { PlasmicCanvasContext } from '@plasmicapp/loader-nextjs'; // or @plasmicapp/host
function MyComponent() {
const inEditor = useContext(PlasmicCanvasContext);
return inEditor ? <MyComponent /> : <MyComponent animated />;
}

Code components for dynamic data

Using code components for showing and interacting with dynamic data is extremely powerful. You can:

  • Create families of components to let users fully design how dynamic data is presented.
  • Display a dynamic set of repeated slot contents (using repeatedElement(), see below).
  • Ensure data is fetched statically or at server render time.

To learn more about this topic, see Using Plasmic with data sources.

More examples

In the following examples, we are showing Codegen’s version of registerComponent(), just because it also shows how to specify the import-related settings like importPath. All the other arguments should be the same as if you were calling PLASMIC.registerComponent().

See the full code component API reference.

Specifying various prop types

Copy
import { SimpleCard } from '../components/SimpleCard';
registerComponent(SimpleCard, {
name: 'SimpleCard',
props: {
// Take simple scalar props
columns: 'number',
// Pass in arbitrary content in the visual editing canvas
children: 'slot',
// You can have any number of slots.
header: 'slot',
// Specify default values for newly inserted elements
animate: {
type: 'boolean',
defaultValue: true
},
// Specify a dropdown of options
category: {
type: 'choice',
options: ['success', 'warning', 'error']
},
// Specify any JSON object
data: 'object'
},
importPath: './components/SimpleCard'
});

Specifying default styles

Copy
import { Menu, Slider, Steps } from 'antd';
// A slider with default styles
registerComponent(Slider, {
name: 'Slider',
props: {
disabled: 'boolean',
vertical: 'boolean'
},
importPath: 'antd',
defaultStyles: {
width: '100%',
maxWidth: '180px'
}
});

Specifying misc metadata

Copy
import * as React from 'react';
import { registerComponent, PlasmicCanvasHost } from '@plasmicapp/host';
import { Menu, Slider, Steps } from 'antd';
import MenuItem from 'antd/lib/menu/MenuItem';
import Head from 'next/head';
import { Step } from 'rc-steps';
import { FancyComponent } from '../components/FancyComponent';
registerComponent(FancyComponent, {
name: 'MyFancyComponent',
// Any human-readable name, for users in Plasmic Studio.
displayName: 'Super fancy component',
// Plasmic usually tries to pass in styles via className, but you can specify
// a different prop name to use. Here, Plasmic will pass styles to
// containerClassName instead.
classNameProp: 'containerClassName',
// The name of the symbol to import from the module path.
importName: 'FancyComponent',
importPath: './components/FancyComponent'
// If it were instead the default export, use this instead of importName.
// isDefaultExport: true,
});

Specifying default slot contents

When inserting a component that has slots, you can specify full templates of element trees that initially fill those slots.

This helps editors to not start with an empty setup; it’s always easier to start with a template that they can then edit. Without this, you’ll often just see an empty box placeholder on the canvas.

This guidance is especially helpful when you have a “family” of components that are meant to work together, such as a Menu of MenuItems.

We highly recommend always providing default slot contents whenever you have slot props! Even if it’s just a text element, it helps to understand what type of content is even supposed to go into the slot.

See the full element type reference.

Copy
// A pair of components, where Menu's children slot accepts only MenuItems.
registerComponent(MenuItem, {
name: 'MenuItem',
displayName: 'Menu Item',
props: {
key: 'string',
children: 'slot'
},
importPath: 'antd/lib/menu/MenuItem',
isDefaultExport: true
});
registerComponent(Menu, {
name: 'Menu',
props: {
mode: 'string',
theme: 'string',
selectedKeys: 'object',
defaultSelectedKeys: 'object',
children: {
type: 'slot',
allowedComponents: ['MenuItem'],
// Default slot contents: two MenuItems
defaultValue: [
{
type: 'component',
name: 'MenuItem',
props: {
key: 'key1',
children: {
type: 'text',
value: 'Menu Option 1'
}
}
},
{
type: 'component',
name: 'MenuItem',
props: {
key: 'key2',
children: {
type: 'text',
value: 'Menu Option 2'
}
}
}
]
}
},
importPath: 'antd'
});

Multiple templates

You can specify multiple templates for editors to insert.

Copy
// Steps component with two templates
registerComponent(Steps, {
name: 'Steps',
props: {
current: 'number',
direction: {
type: 'choice',
options: ['horizontal', 'vertical']
},
percent: 'number',
size: {
type: 'choice',
options: ['default', 'small']
},
type: {
type: 'choice',
options: ['default', 'navigation']
},
children: {
type: 'slot',
allowedComponents: ['Step']
}
},
importPath: 'antd',
templates: {
Basic: {
previewImg: 'https://static1.plasmic.app/steps_template_basic.png',
props: {
direction: 'vertical',
current: 1,
percent: 60,
children: [
{
type: 'component',
name: 'Step',
props: {
title: 'Finished',
description: 'This is a description'
}
},
{
type: 'component',
name: 'Step',
props: {
title: 'In progress',
subTitle: 'Left 00:01:26'
}
},
{
type: 'component',
name: 'Step',
props: {
title: 'Waiting'
}
}
]
}
},
Navigation: {
previewImg: 'https://static1.plasmic.app/steps_template_navigation.png',
props: {
type: 'navigation',
size: 'small',
current: 1,
children: [
{
type: 'component',
name: 'Step',
props: {
title: 'Step 1',
subTitle: '00:00:05',
status: 'finish'
}
},
{
type: 'component',
name: 'Step',
props: {
title: 'Step 2',
subTitle: '00:01:02',
status: 'process'
}
},
{
type: 'component',
name: 'Step',
props: {
title: 'Step 1',
subTitle: 'waiting',
status: 'wait'
}
}
]
}
}
}
});
registerComponent(Step, {
name: 'Step',
props: {
title: 'string',
description: 'string',
subTitle: 'string',
status: {
type: 'choice',
options: ['wait', 'process', 'finish', 'error']
}
},
importPath: 'rc-steps'
});

Applying global themes / contexts to code components

The global CSS styles present in the host page are applied to the code components. For themes that are provided using React contexts, those can be provided when rendering PlasmicCanvasHost in the host page:

Copy
<ThemeContext.Provider>
<PlasmicCanvasHost />
</ThemeContext.Provider>

Any component instances used in your Plasmic designs will have access to the context as per usual.

Evolving code components over time

Plasmic Studio has built-in support for letting you safely evolve your code component metadata over time.

Renaming components.

When you change the name from A to B, the next time you open a project containing instances of A, Plasmic Studio will notice there’s no registration for component A, so it will ask if you renamed it to another component—in which case you should choose B. This will fix up all instances accordingly.

Changing props.

Changing the type of a prop will prompt you to confirm removing usages of the old prop. Note that this removes all existing arguments/values passed into that prop.

Changing the type or name of a prop in a way that carries over all old values passed in for that prop is not yet supported. Tell us if that’s something you’d like to see.

Adding new components and props.

Since these are non-destructive changes, these will immediately take effect in the project without further user interaction.

Deleting components.

If you deleted a component, Plasmic Studio will prompt you if you meant to remove all instances of the component.

Workflow for production vs development

When first setting up app hosting, you initially set your Plasmic project to use a host URL like http://localhost:3000/plasmic-host.

After deploying to production, you typically change this to a URL that anyone can access, such as https://my-app.com/plasmic-host.

But what happens when you want to update/create new code components and test them out?

A simple workflow we recommend is making a temporary (throw-away) clone of your main Plasmic project, setting its host URL once again to http://localhost:3000/plasmic-host, and testing things out there. Once you’re happy with your changes, deploy them to production, and then start using the updates in the original Plasmic project.

(In the future, we may formalize this workflow in the UI, but it will largely be the same under the hood.)

Repeating elements

Code components allow you to bring real data into Plasmic. But say you want to repeat some slot contents based on data—common for collections of things. This will require duplicating the same Plasmic element multiple times. Learn more about this approach to dynamic data.

To repeat slot contents multiple times, use the repeatedElement() function. This ensures an editing experience in Plasmic Studio where the user can select and make edits just the first replica of the slot contents.

Copy
import { repeatedElement } from '@plasmicapp/host';
const ProductCollection = ({
collectionSlug,
children,
className
}: {
children?: ReactNode;
className?: string;
collectionSlug?: string;
}) => {
const data = useFetchProductCollection(collectionSlug);
return (
<div className={className}>
{data?.productList.map((productData, i) => (
<ProductContext.Provider value={productData} key={productData.id}>
{repeatedElement(i === 0, children)}
</ProductContext.Provider>
))}
</div>
);
};

The repeatedElement function expects two parameters: a boolean isPrimary and a React node elt:

  • isPrimary: Indicates the primary copy to be edited in the editor (usually the first copy). Should be true for only one of the copies.
  • elt: The element to be cloned. If the value provided is not a valid ReactElement, the original value is returned. Otherwise, React.cloneElement is used to clone the element, applying required tweaks to elements from the Editor.

Learn more about incorporating dynamic data into your Plasmic designs.

Tree shaking and bundle optimization

(This section is primarily relevant for PlasmicLoader users.)

In our examples so far, we have imported components into plasmic-init (or wherever your PLASMIC object is defined), which in turn is used in all pages that render Plasmic content.

This is fine for many use cases, but it does mean that all components will always be loaded. If you need to optimize your bundle sizes, we recommend colocating your registration calls with the components themselves, and then selectively importing just the components you need into the pages that need them.

For instance, let’s say you have a complex Form component, and you have multiple Plasmic pages including a Contact page. If only Contact needs Form, then you don’t need to import Form into plasmic-init.

components/Form.tsx
Copy
import { PLASMIC } from '../plasmic-init';
export default function Form(props: {}) {
// ...
}
// Co-locate the registration call here, with the component itself.
PLASMIC.registerComponent(Form, {
name: 'Form',
props: {
// ...
}
});

Then you can selectively import that component into just the Contact page. Note that this import statement only specifies the path to the module. This is because we’re making the import only for its side effect. Do not use import Form from '../components/Form' in this case. If you do so then bundlers may optimize the import away.

pages/Contact.tsx
Copy
import '../components/Form';
export default function ContactPage() {
// Ignoring the other context around this component.
return <PlasmicLoader component="ContactPage" />;
}

With the Codegen version of registerComponent(), it already takes metadata about how to import the component, so it will emit the right code and your bundler should automatically be able to tree-shake.

Give feedback on this page