Querying data with code components
One powerful way to extend Plasmic is to create code components that can fetch and render arbitrary data from any data source, including your own product API. This makes it possible to build designs in Plasmic that will be using the same data sources that your production application uses.
Providing data with DataProvider
Here’s an example of a code component that fetches product info for a specific product slug:
import { DataProvider } from '@plasmicapp/loader-nextjs';function ProductBox(props: { children?: ReactNode; className?: string; productSlug?: string }) {const { children, className, productSlug } = props;// A hook that you've defined for fetching product data by slugconst response = useFetchProduct(productSlug);return (<div className={className}>{// Make this data available to this subtree via context,// with the name "product"}<DataProvider name="product" data={response.data}>{children}</DataProvider></div>);}
The above component fetches product data using your own data-fetching React hook, and makes that data available via the special <DataProvider />
component. <DataProvider/>
will then make the data available via React context, so that component instances in the children
will be able to read it.
In order for <DataProvider />
to work, you need to set the parameter providesData
in component registration:
registerComponent(ProductBox, {name: "Product Box",providesData: true,...})
Reading fetched data from code components
You can create code components that read data provided via <DataProvider/>
by other components, using the useSelector()
hook.
For example, here’s a code component that reads the product fetched above, and renders its title:
import { useSelector } from '@plasmicapp/loader-nextjs';function ProductTitle(props: { className?: string }) {const { className } = props;// Selects data named "product"const product = useSelector('product');return <div className={className}>{product?.title ?? 'Product Title'}</div>;}
This component uses the useSelector()
hook to look up data that was provided with the name “product”. It then either renders the product title, if it has been fetched, or the fallback value of “Product Title” if it can’t be found in the context or is not ready yet.
DataProvider vs normal React context
Should you use <DataProvider />
or a normal React context? That depends on your use case. If what you are providing is data that the Plasmic user should have access to from the data picker to build dynamic value expressions, then you should use <DataProvider/>
. If instead it is internal data that is only used by your code components to communicate with each other, then you should use a custom React context.
Using fetched data in dynamic value expressions
<DataProvider/>
is more than just a normal React context provider though — it is a special context provider that the Plasmic Studio understands. When you provide data using <DataProvider />
, that piece of data is also available for dynamic value expressions.
For example, now the Plasmic user will be able to see product
in the data picker:

or write a code expression referencing it. All data provided by the <DataProvider />
is available under the special $ctx
object by its provided name:

Fetching data from your code components
You can fetch your data in your code component however you’d like, but we recommend using @plasmicapp/query
to fetch data in a way that works with both server-side rendering and static site generation.
Using @plasmicapp/query
The library is intentionally barebones; specifically, it only fetches immutable data; it does not support invalidation, mutation, etc. Invalidation and mutation accounts for a lot of complexity from more complete data fetching frameworks like swr or react-query.
@plasmicapp/query provides a few hooks that allow users to tap into this:
usePlasmicDataQuery()
has a similar API asuseSWR()
from swr oruseQuery()
from react-query, taking in a data key and an async fetching function. (We re-export this out of@plasmicapp/loader-react
so users don’t have to know about the@plasmicapp/query
package.)plasmicPrepass()
that users can call to gather pre-fetched data.PlasmicRootProvider
takes aprefetchedQueryData
to pre-populate the query cache.
Example:
// data fetcher componentexport function TweetsProvider(props: {children: React.ReactNode}) {const {children} = props;const {data} = usePlasmicQueryData("/tweets", async () => {const resp = await fetch("https://studio.plasmic.app/api/v1/demodata/tweets");return await resp.json();});if (!data) {return null;}return (<DataProvider name="tweets" data={data}>{children}</DataProvider>);}// Pre-fetchingexport function getStaticProps() {const plasmicData = await PLASMIC.fetchComponentData("Home");const queryCache = await extractPlasmicQueryData(<PlasmicRootProvider loader={PLASMIC} prefetchedData={plasmicData}><PlasmicComponent component="Home" componentProps={...} /></PlasmicRootProvider>);return {props: {plasmicData, queryCache}};}// Rendering pageexport function HomePage({plasmicData, queryCache}) {return (<PlasmicRootProviderprefetchedData={plasmicData}prefetchedQueryData={queryCache}><PlasmicComponent component="Home" componentProps={...} /></PlasmicRootProvider>);}
A few tricky things that plasmicPrepass
does:
- Makes sure
usePlasmicDataQuery()
runs in “suspense” mode, throwing promises, so that react-ssr-prepass can work. usePlasmicDataQuery() otherwise uses non-suspense mode by default. - Looks through
componentProps
, and wraps all found React elements in an error boundary, to make sure react-ssr-prepass can continue even if we encounter rendering errors. Errors can happen if some components expect contextual things to be there but aren’t (like useRouter() for nextjs, etc). We just want to isolate those errors as much as we can to populate the data cache as much as we can. It’s impossible to be perfect here though; some errors will just lead to missing cache entries.
If you don’t want to use @plasmicapp/query
, you can also use other data-fetching frameworks.
Common data-fetching code component patterns
Rendering a wrapping DOM element
In the above example, the ProductBox
component renders a wrapping <div/>
for its children. This is sometimes convenient because this wrapping <div/>
can then serve as a natural place to layout the content. However, you may prefer for your data-fetching component to not render any DOM; in that case, the content will be laid out by the parent element.
function ProductBox(props) {const { children, productSlug } = props;const response = useFetchProduct(productSlug);// Render <DataProvider /> without wrapping divreturn (<DataProvider name="product" data={response.data}>{children}</DataProvider>);}
You can also make this a choice for your user by taking in a prop:
function ProductBox(props) {const { children, productSlug, noLayout, className } = props;const response = useFetchProduct(productSlug);let content = (<DataProvider name="product" data={response.data}>{children}</DataProvider>);if (props.noLayout) {return content;} else {return <div className={className}>{content}</div>;}}
Automatically repeat collection items
If your data fetcher is fetching a collection of items, it may be convenient for your user to automatically repeat the children
using repeatElement
:
function ProductCollection(props: { collectionSlug: string; children?: React.ReactNode }) {const { collectionSlug, children } = props;const data = useFetchProductCollection(collectionSlug);return (<>{data?.productList.map((product, i) => (<DataProvider name="currentProduct" data={product} key={i}>{repeatedElement(i, children)}</DataProvider>))}</>);}
However, sometimes you may want to give the user more control over what content to repeat, or perhaps the data you fetched contains more than just the repeatable collection; it may also include collection name, count of items, and other things that your user may want to use in dynamic data expressions. In that case, you should provide the whole collection, and let the user perform the repetition within the Plasmic studio instead.
Again, you could also leave it up to the user by using a prop:
function ProductCollection(props: { collectionSlug: string; children?: React.ReactNode; noAutoRepeat?: boolean }) {const { collectionSlug, children, noAutoRepeat } = props;const data = useFetchProductCollection(collectionSlug);return (<DataProvider name="collection" data={data}>{noAutoRepeat? children: data?.productList.map((product, i) => (<DataProvider name="currentProduct" data={product} key={i}>{repeatedElement(i, children)}</DataProvider>))}</DataProvider>);}
Single code component vs family of components
Let’s say you want to drop in a section of your landing page that displays product data from your catalog. (You can extend this example to your own situation, such as displaying blog posts from a CMS.)
One approach is to create a single component for that entire section. Content editors can drop in this whole component and customize it via its props, but not edit the design or layout of how the data is shown, as this will all be hard-coded. (This is the example shown on the code components demo site.) This is a great common way to get started, and is sometimes the approach you achieve in CMSes.
Alternatively, you can create a family of components, representing individual pieces of data that Plasmic Studio users can assemble together to create custom designs or layouts of how the entire section is presented. In this scenario, you might create:
- A
ProductBox
component that fetches the data and makes it available in a React Context to its slotchildren
. - Components for
ProductTitle
,ProductImage
,ProductPrice
,ProductDescription
,ProductAddToCartButton
, etc. Users would drop these anywhere within aProductBox
to make it work. - Or even a generic
ProductTextField
component that (via a prop) lets the user choose which product data field they want to display.
This even works for repeated elements.
You can create a ProductCollection
component that takes its children
slot contents and repeats it using repeatedElement()
.
This lets you display a whole collection of products, each of which is rendered using the exact arrangement of ProductTitle
, ProductImage
, etc. that Plasmic Studio users design.
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) => (<DataProvider name="product" data={productData} key={productData.id}>{repeatedElement(i, children)}</DataProvider>))}</div>);};/** Or to display a single product */const ProductBox = ({productSlug,children,className}: {children?: ReactNode;className?: string;productSlug?: string;}) => {const data = useFetchProduct(productSlug);return (<div className={className}><DataProvider name="product" data={data?.productData}>{children}</DataProvider></div>);};const ProductTitle = ({ className }: { className?: string }) => {const productData = useSelector('product');return (<div className={className}>{productData?.title ?? 'This must be inside a ProductCollection or ProductBox'}</div>);};const ProductImage = ({ className }: { className?: string }) => {// ...};
<DataProvider/>
API
Prop | Required? | Description |
---|---|---|
name | Yes | Variable name for the data; must be a valid javascript identifier; users will see this name in the data picker, and can reference this as code expressions as $ctx.name |
data | Yes | Data to provide; can be undefined if not available |
hidden | No | Hides the variable from the data picker UI and $ctx ; useful when you are providing data that your other code components may want to consume, but that you don’t want the user to use directly |
label | No | A nicer human-friendly label that will be used instead in the data picker UI |
children | Yes | React tree that will have access to the provided data |
Advanced Topics
Background: approaches to data fetching
The easiest way to get data-fetching components working is to make them perform their own fetching.
This is easy to do client-side using useEffect()
or your choice of data fetching library such as react-query or react-swr.
However, for statically generated pages or server-rendered pages, you may want to ensure these components fetch data statically at pre-render time.
There are two approaches to this.
For any Plasmic designs that need data, the developer ensures that (in the code) the needed data is fetched statically at the page level (using the usual mechanisms such as
getStaticProps
for Next.js), and then rely on React Context to provide the data to any dependent components. This does require you to (redundantly) specify in the page load phase the same set of data dependencies—an issue that is not specific to Plasmic.(Recommended) Continue having components fetch their own data. To make this easy, Plasmic provides a simple data-fetching library,
@plasmicapp/query
, that can perform isomorphic data fetching from within any component (using react-swr under the hood). It is based on react-ssr-prepass, and provides Suspense-style data fetching from any component, not just fromgetStaticProps
orgetServerSideProps
.
Using a different data-fetching library
You can also any other data-fetching library instead of @plasmicapp/query
or swr
(which @plasmicapp/query
uses under the hood).
You can do so and still take advantage of plasmicPrepass
.
In short, ignore extractPlasmicQueryData
and use plasmicPrepass
instead.
Here’s an example with swr
(note that this is a silly example because @plasmicapp/query
already uses swr
under the hood, so it is only for illustrative purposes):
// data fetcher componentexport function TweetsProvider(props: {children: React.ReactNode}) {const {children} = props;const {data} = useSWR("/tweets", ...);return (<DataProvider name="tweets" data={data}>{children}</DataProvider>);}// Pre-fetchingexport function getStaticProps() {const plasmicData = await PLASMIC.fetchComponentData("Home");const cache = new Map();await plasmicPrepass(<SWRConfig value={{provider: () => cache, suspense: true}}><PlasmicRootProvider loader={LOADER} prefetchedData={plasmicData}><PlasmicComponent component="Home" componentProps={...} /></PlasmicRootProvider></SWRConfig>);const queryCache = Object.fromEntries(cache.entries());return {props: {plasmicData, queryCache}};}// Rendering pageexport function HomePage({plasmicData, queryCache}) {return (<SWRConfig value={{fallback: queryCache}}><PlasmicRootProviderprefetchedData={plasmicData}><PlasmicComponent component="Home" componentProps={...} /></PlasmicRootProvider></SWRConfig>);}
Use with codegen
The above shows how to make things work using the Headless API, but you can also make it work with codegen.
You can manually install @plasmicapp/query
and implement their extractPlasmicQueryData
method.
It could be something like:
data/data.tsxexport async function extractPlasmicQueryData(element: React.ReactElement): Promise<Record<string, any>> {const cache = new Map<string, any>();try {await prepass(<PlasmicPrepassContext cache={cache}>{element}</PlasmicPrepassContext>);} catch (err) {console.warn(`PLASMIC: Error encountered while pre-rendering`, err);}return Object.fromEntries(Array.from(cache.entries()).filter(([key, val]) => !key.startsWith('$swr$') && val !== undefined));}
And then you can use it normally when rendering the pages (you can use PlasmicQueryDataProvider directly):
pages/myPage.tsximport { PlasmicQueryDataProvider } from '@plasmicapp/query';import * as React from 'react';import { PlasmicMyPage } from '../components/plasmic/PlasmicMyPage';import { extractPlasmicQueryData } from '../data/data';export async function getStaticProps() {const queryCache = await extractPlasmicQueryData(<PlasmicMyPage />);return { props: { queryCache } };}function MyPage({ queryCache }: Record<string, any>) {return (<PlasmicQueryDataProvider prefetchedCache={queryCache}><PlasmicMyPage /></PlasmicQueryDataProvider>);}export default MyPage;
Optional: You can add a GenericErrorBoundary
to fetch as much data as possible even in the face of rendering errors.
data/data.tsximport { PlasmicPrepassContext } from '@plasmicapp/query';import React from 'react';import prepass from 'react-ssr-prepass';export async function extractPlasmicQueryData(element: React.ReactElement): Promise<Record<string, any>> {const cache = new Map<string, any>();try {await plasmicPrepass(<PlasmicPrepassContext cache={cache}>{element}</PlasmicPrepassContext>);} catch (err) {console.warn(`PLASMIC: Error encountered while pre-rendering`, err);}return Object.fromEntries(Array.from(cache.entries()).filter(([key, val]) => !key.startsWith('$swr$') && val !== undefined));}/*** Runs react-ssr-prepass on `element`*/async function plasmicPrepass(element: React.ReactElement) {await prepass(buildPlasmicPrepassElement(element));}/*** Unfortunately in codegen we can't check for `PlasmicComponent` instances,* making it harder to isolate components in error boudaries to fetch as much* data as possible.*/function buildPlasmicPrepassElement(element: React.ReactElement) {return <GenericErrorBoundary>{element}</GenericErrorBoundary>;}class GenericErrorBoundary extends React.Component<{children: React.ReactNode;}> {constructor(props: { children: React.ReactNode }) {super(props);}componentDidCatch(error: any) {console.log(`Plasmic: Encountered error while prepass rendering:`, error);}render() {return this.props.children;}}