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:

Copy
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 slug
const 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:

Copy
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:

Copy
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:

Using provided product in data picker UI

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

Using provided product in data picker UI

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 as useSWR() from swr or useQuery() 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 a prefetchedQueryData to pre-populate the query cache.

Example:

Copy
// data fetcher component
export 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-fetching
export 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 page
export function HomePage({plasmicData, queryCache}) {
return (
<PlasmicRootProvider
prefetchedData={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.

Copy
function ProductBox(props) {
const { children, productSlug } = props;
const response = useFetchProduct(productSlug);
// Render <DataProvider /> without wrapping div
return (
<DataProvider name="product" data={response.data}>
{children}
</DataProvider>
);
}

You can also make this a choice for your user by taking in a prop:

Copy
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:

Copy
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:

Copy
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.

data product

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 slot children.
  • Components for ProductTitle, ProductImage, ProductPrice, ProductDescription, ProductAddToCartButton, etc. Users would drop these anywhere within a ProductBox 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.

data product box

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.

data product collection

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) => (
<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

PropRequired?Description
nameYesVariable 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
dataYesData to provide; can be undefined if not available
hiddenNoHides 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
labelNoA nicer human-friendly label that will be used instead in the data picker UI
childrenYesReact 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 from getStaticProps or getServerSideProps.

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):

Copy
// data fetcher component
export function TweetsProvider(props: {children: React.ReactNode}) {
const {children} = props;
const {data} = useSWR("/tweets", ...);
return (
<DataProvider name="tweets" data={data}>
{children}
</DataProvider>
);
}
// Pre-fetching
export 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 page
export function HomePage({plasmicData, queryCache}) {
return (
<SWRConfig value={{fallback: queryCache}}>
<PlasmicRootProvider
prefetchedData={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.tsx
Copy
export 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.tsx
Copy
import { 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.tsx
Copy
import { 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;
}
}
Was this page helpful?

Give feedback on this page