Querying data with code components

To enable your Plasmic Studio users to drag-and-drop in data from your data source, you can create code components that let fetch and render this data.

In this approach, the workflow looks like:

  • Developer registers code components for use in Plasmic. These components would be rendering data from the data source.
  • Plasmic Studio users create a page, dragging and dropping the components into the page.
  • Plasmic Studio users can directly publish pages. No subsequent developer involvement is necessary.

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) => (
<ProductContext.Provider value={productData} key={productData.id}>
{repeatedElement(i === 0, children)}
</ProductContext.Provider>
))}
</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}>
<ProductContext.Provider value={data?.productData}>{children}</ProductContext.Provider>
</div>
);
};
const ProductTitle = ({ className }: { className?: string }) => {
const productData = useContext(ProductContext);
return (
<div className={className}>{productData?.title ?? 'This must be inside a ProductCollection or ProductBox'}</div>
);
};
const ProductImage = ({ className }: { className?: string }) => {
// ...
};

Static or server-rendered data fetching

Summary: Use @plasmicapp/query to fetch data from any component in a way that works with server-side rendering and static site generation

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 @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 (
<>
{data.map((tweet, i) => (
<TweetContext.Provider value={tweet} key={i}>
{repeatedElement(i === 0, children)}
</TweetContext.Provider>
)}
</>
);
}
// 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.

Using a different data-fetching library

You can also any other data-fetching library instead of @plasmicapp/query or react-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 react-swr (note that this is a silly example because @plasmicapp/query already uses react-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", ...);
}
// 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;
}
}

Give feedback on this page