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.
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:
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:
import { PLASMIC } from "../plasmic-init";import { extractPlasmicQueryData, PlasmicRootProvider, PlasmicComponent } from "@plasmicapp/loader-nextjs";// Pre-fetchingexport function getStaticProps({params}) {const plasmicData = await PLASMIC.fetchComponentData("Home");const queryCache = await extractPlasmicQueryData(<PlasmicRootProviderloader={PLASMIC}prefetchedData={plasmicData}pageParams={params}><PlasmicComponent component="Home" componentProps={...} /></PlasmicRootProvider>);return {props: {plasmicData, queryCache}};}// Rendering pageexport function HomePage({plasmicData, queryCache}) {const pageMeta = plasmicData.entryCompMetas[0];const router = useRouter();return (<PlasmicRootProviderprefetchedData={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 yourextractPlasmicQueryData()
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:
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 replaceuseRouter()
withuseSafeRouter()
, and have your code component handle the case whenuseSafeRouter()
returnsundefined
:
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 thepageParams
prop into<PlasmicRootProvider/>
, then you can access them like this:
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.
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 }) => {// ...};
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:
<QueryContext.Provider value={{query: () => pg.query()}}>
In your render function, provide a normal fetch to your /api:
<QueryContext.Provider value={{query: () => fetch('/api/query', {...opts})}}>
In your data fetching code component:
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()
andextractPlasmicQueryData()
, that can perform isomorphic data fetching from within any component (usingswr
under the hood). The data extraction is based onreact-ssr-prepass
, and provides Suspense-style data fetching from any component, not just fromgetStaticProps
orgetServerSideProps
.
Have feedback on this page? Let us know on our forum.