Closed angeloashmore closed 2 years ago
This looks fantastic, Angelo. I definitely agree with the need and reasoning behind implementing this. The implementation looks great, too. I'm all for it.
Hey @angeloashmore the only suggestion that I would have is to consider fetching data with the slice zone, in term of vision we think this can fit most of the use cases and simplify the overall development experience for newcomers. You drop a slice-zone and then focus on your components.
Thanks for the review @Duaner.
I see this as a low-level component that does not implement data fetching because data fetching usually requires nuanced implementations. I think we would build integrations around this component in order to fulfill the vision you describe (and I agree with!).
For example, to take full advantage of Next.js, data fetching needs to happen in getStaticProps
, getServerSideProps
, or getInitialProps
. If the component did data fetching, it would only happen in the browser at run time, not the server. This integration would be in something like a new @prismicio/next
kit, or directly in Slice Machine's Next.js package.
We have a similar issue with static site generators like Gatsby. Data fetching is done in a custom GraphQL API implemented by the framework. This allows Gatsby to ensure build-time data is properly split between pages for faster loading. Again, if the component fetched its own data, it would only happen in the browser at run time. In this specific case, there also isn't a way to automatically fetch data from Gatsby's GraphQL API from within a component. There may not be a kit created for Gatsby, but rather a shared best practice on structuring the project using this SliceZone component.
I think we have to look at specific use-cases to learn how to implement this component and the ecosystem around it. I've started to do that. I can create a quick diagram showing the overall architecture of the JavaScript packages and how they relate to each other, all with a focus on simplifying, but also being explicit.
<SliceZone>
component fit into React and its higher-level frameworks like Next.js and Gatsby?Read on…
Things to keep in mind
Users may have expectations of how data should flow in their applications. They know where to fetch data. They know where to display data.
If users don't know how their framework works, we should teach them how to best use it. We should promote sustainable practices for long-term success and satisfaction. This involves working within a framework's conventions.
Well thought out lower-level APIs have been developed. We should use them. There isn't a need to re-implement simpler APIs if the underlying libraries are already simple.
Fetch a document from an API. The document includes Slice Zones and any document-level fields.
The method with which the document is fetched varies by framework. Some require specialized methods for optimization purposes, such as server side rendering.
API requests can be performed in any way, whether through the REST API, GraphQL API, or through client libraries, like @prismicio/client
. We would recommend the best method for the framework.
Render that document. Full access to the document's data is necessary to access document-level fields. This is needed for setting content such as SEO data, page titles, and redirects. Slices are just one part of the document.
That being said, Slices are arguably the most important part of the document. As such, rendering that content should be easy.
The Slice Zone React component is designed to work in any React app. This includes create-react-app, Next.js, and Gatsby.
How a user uses it, however, will be framework-specific due to different data fetching requirements. Next.js users, for example, will fetch documents via getStaticProps
where Gatsby users will query its internal GraphQL API.
The following diagram shows how data would flow through different frameworks and libraries.
For the following examples, assume the following components and types are available.
getStaticProps
and getServerSideProps
are Next.js's recommended data fetching APIs. Any server-side code can be called here that returns an object which will be passed to the page. Fetching data in these APIs ensures data is not fetched during client-side rendering, delivering a faster experience for users.
In the following code example, getStaticProps
is used to fetch a document and pass it to the page. It is fully typed using TypeScript with @prismicio/types
.
import * as nextT from 'next/types'
import * as prismicNext from '@prismicio/next'
import * as prismicH from '@prismicio/helpers'
import { SliceZone } from '@prismicio/react'
import { PrismicPage } from '../types'
import { createClient } from '../prismic'
import { PageBodyText } from '../components/PageBodyText'
import { PageBodyImageGallery } from '../components/PageBodyImageGallery'
import { PageBodyQuote } from '../components/PageBodyQuote'
type PageParams = {
uid: string
}
type PageProps = {
page: PrismicPage
}
export default function Page({ page }: PageProps) {
return (
<>
<Head>
<title>{prismicH.asText(page.data.title)}</title>
</Head>
<SliceZone
slices={page.data.body}
components={{
text: PageBodyText,
image_gallery: PageBodyImageGallery,
quote: PageBodyQuote,
}}
/>
</>
)
}
export const getStaticProps: GetStaticProps<PageProps, PageParams> = async (
context,
) => {
// Assume createClient() returns a pre-configured @prismicio/client instance.
const client = createClient()
prismicNext.enableClientServerSupportFromContext(client, context)
const uid = context.params.uid
const page = await client.getByUID<PrismicPage>('page', uid)
return {
props: { page },
}
}
Gatsby requires a unique approach to data fetching. It aggregates data from multiple sources configured by a user and exposes it as an internal GraphQL API. It has a strong emphasis on static site generation and encourages users to perform work during build-time.
In the following code example, query
is used to fetch a document and pass it to the page. It is fully typed using TypeScript with a code generator like graphql-code-generator
.
Notice that the component is nearly identical to Next.js. The example differs primarily in its data fetching, but follows the same concept.
import * as React from 'react'
import { graphql, PageProps } from 'gatsby'
import { Helmet } from 'react-helmet-async'
import { SliceZone } from '@prismicio/react'
import { PageTemplateQuery } from '../types'
import { PageBodyText } from '../components/PageBodyText'
import { PageBodyImageGallery } from '../components/PageBodyImageGallery'
import { PageBodyQuote } from '../components/PageBodyQuote'
type PageContext = {
id: string
}
type PageProps = PageProps<PageTemplateQuery, PageContext>
export default function Page({ data }: PageProps) {
const page = data.prismicPage
return (
<>
<Helmet>
<title>{page.data.title.text}</title>
</Helmet>
<SliceZone
slices={page.data.body}
components={{
text: PageBodyText,
image_gallery: PageBodyImageGallery,
quote: PageBodyQuote,
}}
/>
</>
)
}
export const query = graphql`
query PageTemplate($id: String!) {
prismicPage(id: { eq: $id }) {
data {
title {
text
}
body {
... on PrismicPageDataText {
primary {
text {
raw
}
}
}
... on PrismicPageDataImageGallery {
items {
image {
url
alt
}
caption
}
}
... on PrismicPageDataQuote {
primary {
quote {
raw
}
quotee
}
}
}
}
}
}
`
React is the underlying UI layer for Next.js and Gatsby. Many users choose to use it directly.
In the following code example, usePrismicClient
is used to fetch a document and pass it to the return value of the component. It is fully typed using TypeScript with @prismicio/types
.
This example could be futher developed with Suspense for proper data loading, but it should illustrate the SliceZone
component sufficiently.
import * as React from 'react'
import * as prismicH from '@prismicio/helpers'
import { SliceZone } from '@prismicio/react'
import { PrismicPage } from '../types'
import { client } from '../prismic'
import { PageBodyText } from '../components/PageBodyText'
import { PageBodyImageGallery } from '../components/PageBodyImageGallery'
import { PageBodyQuote } from '../components/PageBodyQuote'
export default function Page() {
const [page, setPage] = React.useState<PrismicPage>()
useEffect(() => {
const asyncFn = async () => {
const uid = 'uid' // Get the page UID somehow
const document = await client.getByUID<PrismicPage>('page', uid)
setPage(document)
}
asyncFn()
}, [])
if (!page) {
return <span>Loading</span>
}
return (
<>
<Head>
<title>{prismicH.asText(page.data.title)}</title>
</Head>
<SliceZone
slices={page.data.body}
components={{
text: PageBodyText,
image_gallery: PageBodyImageGallery,
quote: PageBodyQuote,
}}
/>
</>
)
}
@prismicio/react@v2.0.0
was published with the <SliceZone>
described above (see #97) so I'm going to close this issue.
Thanks for your feedback, everyone!
RFC: Generic React SliceZone component
In this RFC, I propose a generic React
SliceZone
component be created and used in all React, Gatsby, and Next.js projects using Prismic Slice Zones.Creating a standard component leads to a consistent API design across framework integrations. It also leads to consistent and shared education efforts.
Consider the following existing implementations of Slice Zone components when reading this RFC to understand how the packages could be compared and integrated:
prismic-gatsby-coffee-sample
gatsby-getting-started-tutorial
gatsby-blog
reactjs-blog
react-map-to-components
(prior art)A general
SliceZone
component must have the following attributes:To achieve this, clear boundries of this component's functionality must be set.
What this component will do:
What this component will not do:
Example implementation
The following example implementation makes use of the new experimental
@prismicio/types
library. The library contains shared TypeScript types and interfaces that can be used across TypeScript projects. In this example, we're using it for its genericSlice
type.Items of note:
SliceComponentProps
is the normalized React props interface for all Slice React components. It contains the full Slice data object and an optional user-defined context value. The context value can contain anything a user needs to send down to a Slice's component.SliceZoneProps
is properly typed, but not too strictly. It accepts a list of Slice objects, a record of Slice types mapped to React components, a default component if the map does not contain a component for a Slice, and an optional user-provided context value.MissingSlice
provides helpful information during development and is used as the default Slice component.SliceZone
is overwhelmingly simple. Its flexibility comes in the way of using JavaScript's language features.Example Slice component
Like above, we are using
@prismicio/types
for its genericSlice
type and collection of Prismic field types.It references the
SliceComponentProps
interface defined in the above example.Component flexibility
In the previous example,
SliceComponentProps
is used to type the components. Slice component do not need to adhere to this interface. It is, however, what will be passed to the provided component as part ofSliceZone
's rendering process.To use a different prop interface for the Slice component, one can adjust the
component
map provided toSliceZone
.If such a behavior becomes common within an application, an abstraction can be created. Ideally this abstraction is created by a user, not provided by the library.
Generatable
To be viable, this component must work with generated code. Inversely, the code required by the component must be easy to generate. These requirements come from the need to integrate this component with Slice Machine.
@prismicio/types
to simplify generated types.Final comments
This general component is low-level enough to be used in any React project using Prismic. It does not require TypeScript usage, but users will benefit greatly through its use.
Topics like default props (linked issue is about Vue.js but applies here as well) become something that can be handled via React, not the Slice Zone component.
None of the code presented here has been tested or run. Please don't paste this into a file and wonder why it doesn't work. 😅 This is all open for discussion!