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.

Plasmic also provides a simple framework for convenient fetching at pre-render time (during static site generation or server side rendering), so that your pages can be rendered with the data already available, and without incurring any browser-side network requests. However, if the fetch has any runtime dependencies (such as on some React state), it can also fetch data at runtime.

Summary

This code shows how to write a code component that can fetch data at pre-render (SSG/SSR) time and provide that data to its descendants.

Highlights:

  • It can fetch data at pre-render time (SSR/SSG).
  • It can also re-fetch data at runtime if needed (e.g. if it depends on some React state).
  • It can depend on props that vary from instance to instance or at runtime.
  • Simply insert this component anywhere, without needing to update a page-level method like getStaticProps/getServerSideProps.
  • DataProvider makes the data usable in Plasmic Studio via dynamic value expressions, or from other code components.
Copy
export function TweetsProvider({ children }: { children: React.ReactNode }) {
const { data } = usePlasmicQueryData('/tweets', async () => {
const resp = await fetch('https://studio.plasmic.app/api/v1/demodata/tweets');
return await resp.json();
});
return (
<>
{data && (
<DataProvider name="tweets" data={data}>
{children}
</DataProvider>
)}
</>
);
}

Fetching data from your code components

You can fetch your data in your code component however you’d like, but we recommend using hooks provided by Plasmic to fetch data in a way that works with both server-side rendering and static site generation.

Plasmic provides a usePlasmicDataQuery() hook that has a similar API as useSWR() from swr or useQuery() from react-query, taking in a data key and an async fetching function. You can use it fetch data from your code components, like this:

components/TweetsProvider.tsx
Copy
import { usePlasmicQueryData } from '@plasmicapp/loader-nextjs';
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-rendering query data

Often, you would want to perform all the data-fetching at build time, so that the queried data will be embedded in the html, and your pages will render without having to wait for a data fetch. Plasmic provides a extractPlasmicQueryData() function that allows you to extract data queried using usePlasmicQueryData() from a React element tree. You can use it at build time to extract the necessary data, then provide the pre-fetched data via a context into your app.

For example, for Next.js, you would do the data fetching in getStaticProps(), like so:

Copy
import { PLASMIC } from "../plasmic-init";
import { extractPlasmicQueryData, PlasmicRootProvider, PlasmicComponent } from "@plasmicapp/loader-nextjs";
// Pre-fetching
export function getStaticProps({params}) {
const plasmicData = await PLASMIC.fetchComponentData("Home");
const queryCache = await extractPlasmicQueryData(
<PlasmicRootProvider
loader={PLASMIC}
prefetchedData={plasmicData}
pageParams={params}
>
<PlasmicComponent component="Home" componentProps={...} />
</PlasmicRootProvider>
);
return {
props: {plasmicData, queryCache}
};
}
// Rendering page
export function HomePage({plasmicData, queryCache}) {
const pageMeta = plasmicData.entryCompMetas[0];
const router = useRouter();
return (
<PlasmicRootProvider
prefetchedData={plasmicData}
prefetchedQueryData={queryCache}
pageParams={router.params}
>
<PlasmicComponent component="Home" componentProps={...} />
</PlasmicRootProvider>
);
}

How extractPlasmicQueryData() works

We extract data from your React element tree by running react-ssr-prepass, which is a technique for “fake-rendering” a React element tree by using a fake React runtime. This runtime can detect fetches performed by usePlasmicQueryData() (as thrown promises), wait for those fetches to resolve, and save the results into a cache. This cache can then be used at render time; usePlasmicQueryData() will only actually perform a fetch if the data it wants is not in this cache.

Note that the element tree you pass into extractPlasmicQueryData() doesn’t need to be exactly like what you’re actually rendering for the final page; it just needs to render enough to fire off all the data fetches you need to do. But because this “prepass rendering” phase is happening in a fake React runtime, and not the usual Next.js runtime, you may need to take care of a few things:

  • If you have code components that read from your React contexts, and the context providers are in places like _app.tsx, you’ll need to either also need to include the context providers in your extractPlasmicQueryData() call, or make sure your code components can work correctly when those context values are not provided.

  • If you have code components that use useRouter(), this will thrown an error in the fake runtime. See next section for workarounds!

Workarounds for useRouter()

If you’re using Next.js, then during prepass, you may see an error like this:

Copy
PLASMIC: Encountered error when pre-rendering SOME_COMPONENT: Error: invariant expected app router to be mounted

or something similar. The reason is that you have code components that calls useRouter() but the Next.js router is not mounted in the element tree you’re passing into extractPlasmicQueryData(), resulting in this error. This doesn’t completely break extractPlasmicQueryData(); it just means it won’t be able to extract query data from rendering SOME_COMPONENT.

There are a few workarounds for this:

  • If you have no data fetching going on within SOME_COMPONENT, then you can just ignore the warning.
  • If you do have data fetching beneath this code component tree, but the information from useRouter() does not impact the data that’s fetched, then you can define something like this, and replace useRouter() with useSafeRouter(), and have your code component handle the case when useSafeRouter() returns undefined:
Copy
export function useSafeRouter() {
try {
return useRouter();
} catch {
return undefined;
}
}
  • If your code component does perform data fetches that relies on information from useRouter(), then you’ll need to get the url params through some other means. For example, if you followed the instructions above and passed in the pageParams prop into <PlasmicRootProvider/>, then you can access them like this:
Copy
import { useSelector } from '@plasmicapp/loader-nextjs';
export function useSafeParams() {
const dataCtxParams = useSelector('params');
try {
return useRouter().params;
} catch {
return dataCtxParams;
}
}

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/react-web/lib/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 }) => {
// ...
};

Fetching from your own APIs

Generally, this approach works well for APIs that are third-party.

What if you want to query your own API handlers in a framework like Next.js (under /api)?

If you are performing SSR (e.g. getServerSideProps), then you can still make the call, but node fetch will require fully qualified URLs—rather than query /api/foo, you’ll need to query https://example.com/api/foo or http://localhost:3000/api/foo to query via loopback device (ideally, this hostname should be externally supplied by the environment).

If you are performing SSG (e.g. getStaticProps), the API handlers will not even be running! So instead of querying your own /api, you can instead directly execute the handlers your API routes bind to.

What if you need to keep this isomorphic so that the components can query from both browser and SSG? Say for instance you are performing a Postgresql database query. It is bound to /api/query and lives in a handler function query(). You cannot simply define a function isomorphicQuery() that checks if window is defined and switch between the two - the database modules like pg will end up getting bundled with your client code. You will need to inject this as a dynamic dependency, e.g. via React context.

So for instance:

In getStaticProps, in extractPlasmicQueryData, use:

Copy
<QueryContext.Provider value={{query: () => pg.query()}}>

In your render function, provide a normal fetch to your /api:

Copy
<QueryContext.Provider value={{query: () => fetch('/api/query', {...opts})}}>

In your data fetching code component:

Copy
const { query } = useContext(QueryContext);
const data = usePlasmicQueryData('myquery', query);

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 libraries, usePlasmicQueryData() and extractPlasmicQueryData(), that can perform isomorphic data fetching from within any component (using swr under the hood). The data extraction is based on react-ssr-prepass, and provides Suspense-style data fetching from any component, not just from getStaticProps or getServerSideProps.

Was this page helpful?

Have feedback on this page? Let us know on our forum.