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.
// Data pre-fetched during SSG or SSR - make sure to use extractPlasmicQueryData() in your getStaticProps or getServerSideProps.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 @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.)extractPlasmicQueryData()
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 extractPlasmicQueryData
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/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 }) => {// ...};
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
that powers extractPlasmicQueryData
.
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;}}