prismicio / prismic-react

React components and hooks to fetch and present Prismic content
https://prismic.io/docs/technologies/homepage-reactjs
Apache License 2.0
154 stars 40 forks source link

RFC: @prismicio/react refresh #92

Closed angeloashmore closed 2 years ago

angeloashmore commented 2 years ago

Overview

This request for comments (RFC) presents a collection of React components and hooks to make presenting and fetching Prismic content easy and extendable. Some of the described components are updates to the existing prismic-reactjs library, while others are new.

These ideas have been developed as a result of real-world use in different environments including within agencies and in-house development teams.

The proposals defined in this RFC are designed for all React users. Some are general enough to be used in frameworks (such as Next.js and Gatsby), while others are recommended for non-framework use. Each section includes a "Designed to be used with" label to identify for whom it is designed.

Background Information

prismic-reactjs currently includes the following exports:

Components

Functions

This API combines a React Component with a collection of helper functions from @prismicio/helpers and @prismicio/richtext. The combination of components and functions leads to non-idiomatic React code, mixing JSX (e.g. <RichText>) with JavaScript expressions (e.g. {RichText.asText()}).

Components provide a way to encapsulate functionality. The current library does not fully utilize this concept. Link.url(), for example, could be integrated into a <Link> component which performs the URL transformation implicitly. Because Link.url() is simply a re-exported helper from @prismicio/helpers, users could import that library instead if such lower-level functionality is needed.

Proposal

The following components and hooks make for a more robust React integration with Prismic.

Each component and hook is described below.

Rename to @prismicio/react

To mirror recent efforts to streamline Prismic's library naming, the library should be renamed from prismic-reactjs to @prismicio/react.

The following Prismic libraries also follow this convention:

<PrismicProvider>

Designed to be used with: React w/o a framework, Next.js, Gatsby

This Context Provider sets up app-wide configuration for the library's components and hooks. It can contain an app's @prismicio/client instance, Link Resolver, Rich Text components, and a router-specific Link component. See the following component and hook descriptions to learn how they are used.

Using the provider is optional. If it is not used, components and hooks can be configured where they are used.

In cases where multiple repositories are used to gather content, or when location-specific overrides are needed, values provided to the provider have a lower priority than configuration provided to a component or hook directly.

import * as prismic from '@prismicio/client'
import { PrismicProvider, PrismicLink } from '@prismicio/react'
import { Link } from 'react-router-dom'

import { linkResolver } from '../linkResolver'
import { Heading } from './Heading'

const endpoint = prismic.getEndpoint('qwerty')
const client = prismic.createClient(endpoint)

const richTextComponents = {
  h1: 'h2',
  h2: Heading,
  h3: (props) => <Heading as="h4" {...props} />,
}

const App = ({ children }) => {
  return (
    <PrismicProvider
      client={client}
      linkResolver={linkResolver}
      richTextComponents={richTextComponents}
      internalLinkComponent={Link}
    >
      {children}
    </PrismicProvider>
  )
}

<SliceZone>

Designed to be used with: React w/o a framework, Next.js, Gatsby

See the dedicated <SliceZone> RFC: https://github.com/prismicio/prismic-reactjs/issues/86

<SliceZone
  slices={document.data.body}
  components={{
    foo: FooSlice,
    bar: BarSlice,
  }}
/>

<RichText>

Designed to be used with: React w/o a framework, Next.js, Gatsby

This component renders an HTML representation of a Rich Text field. It automatically converts links using a provided Link Resolver. By default, it renders a standard set of HTML elements (e.g. Heading 1 becomes <h1>), but can be overridden using a list of components.

The component to be rendered can be provided in several ways:

  1. As a string denoting an HTML element (e.g. "h2").
  2. As a direct reference to a React component (e.g. Heading).
  3. As an inline React component definition (e.g. (props) => <Heading as="h4" {...props} />)

This provides users an easy and flexible way to override the rendered components. This replaces the HTML Serializer concept with a simpler approach without removing capabilities.

<RichText
  field={document.data.content}
  linkResolver={linkResolver}
  components={{
    heading1: 'h2',
    heading2: Heading,
    heading3: (props) => <Heading as="h4" {...props} />,
    hyperlink: (props) => <PrismicLink internalComponent={Link} {...props} />,
  }}
/>

If <PrismicProvider> is used and configured with a richTextComponents prop, it will be used as <RichText>'s components prop. If a components prop is provided to <RichText>, it will take priority over the prop provided to <PrismicProvider>. The same behavior is included for linkResolver.

<RichText field={document.data.content} />

<RichTextAsText>

Designed to be used with: React w/o a framework, Next.js, Gatsby

This component renders a text representation of a Rich Text field. It serves as the "non-rich" counterpart to the <RichText> component as it strips all formatting and HTML representations within the field's contents.

<RichTextAsText field={document.data.content} />

Since the output is a string, no customization aside from the field prop is necessary.

<PrismicLink>

Designed to be used with: React w/o a framework, Next.js, Gatsby

This component renders a link from a Prismic Link field. It supports internal and external URLs and can render the appropriate component accordingly. If a link is internal to an app, the app may require a special <Link> component to be rendered rather than an <a> element. <PrismicLink> will automatically switch to that component as needed.

<PrismicLink
  field={document.data.link}
  linkResolver={linkResolver}
  internalComponent={Link}
  externalComponent="a"
>
  Learn More
</PrismicLink>

If the link is configured with target="_blank", which can be set within the Prismic editor, rel="noopener noreferrer" will be added for improved security. Users can opt-out by providing their own rel prop.

For <RichText> and Gatsby compatibility, PrismicLink can take an href directly. The href prop accepts a URL string that has already gone through the app's Link Resolver or an external URL. The URL can be internal or external with the same automatic adaption described previously.

// Renders externalComponent
<PrismicLink href="https://example.com">
  Learn More
</PrismicLink>

// Renders internalComponent
<PrismicLink href="/about">
  Learn More
</PrismicLink>

If <PrismicProvider> is used and configured with an internalLinkComponent prop, it will be used as <PrismicLink>'s internalComponent prop. If an internalComponent prop is provided to <PrismicLink>, it will take priority over the prop provided to <PrismicProvider>. The same behavior is included for externalComponent.

<PrismicLink field={document.data.link}>
  Learn More
</PrismicLink>

<PrismicToolbar>

Designed to be used with: React w/o a framework, Next.js, Gatsby

This component makes it convenient to add the Prismic Toolbar to an app. It automatically generates the correct script URL and includes the recommended configuration.

<PrismicToolbar repositoryName="qwerty" type="new" />

The output would be equivalent to the following HTML.

<script
  src="https://static.cdn.prismic.io/prismic.js?repo=qwerty&new=true"
  defer="true"
/>

usePrismicDocument(), et al.

Designed to be used with: React w/o a framework

A collection of React hooks, including usePrismicDocuments(), usePrismicDocumentByID(), and usePrismicDocumentsByType(), queries the Prismic REST API for content from a Prismic repository.

The API mirrors that of the @prismicio/client library in hook form. It handles different states automatically, such as loading states and data persistence between re-renders. API parameters, such as lang and orderings, are provided as each hook's last parameter, just like using the client directly.

The following hooks fetch one or more documents. They return Prismic's paginated API responses which can be helpful when displaying paginated content.

The following list of hooks is a variation of the previous list. These hooks automatically fetch all documents from a paginated response and may make multiple network requests in order to fetch all matching documents.

The following hooks return one document.

All hooks return the following data shape:

import * as prismic from '@prismicio/client'
import { usePrismicDocument } from '@prismicio/react'

const endpoint = prismic.getEndpoint('qwerty')
const client = prismic.createClient(endpoint)

const MyComponent = () => {
  const { data, isLoading, error } = usePrismicDocumentByUID('page', 'home', {
    client
  })

  return <span>My component</span>
}

The @prismicio/client instance is provided using the client option in the hooks' last parameter.

import * as prismic from '@prismicio/client'
import { usePrismicDocumentByUID } from '@prismicio/react'

const endpoint = prismic.getEndpoint('qwerty')
const client = prismic.createClient(endpoint)

const MyComponent = () => {
  const { data, isLoading, error } = usePrismicDocumentByUID('page', 'home', {
    client,
  })

  return <span>My component</span>
}

If <PrismicProvider> is used and configured with a client prop, it will be used as the client option. If a client option is provided to a hook, it will take priority over the prop provided to <PrismicProvider>.

How to provide feedback

This is a public request for comments on the proposed ideas. If you have any feedback or suggestions, please feel free to reply below.

We would like to hear from potential users of these ideas whether the ideas positively or negatively impact workflows. If you have any additional ideas as well, please share them here.

Everything posted here is open for feedback and is not final. Development may have already begun by the time you are reading this, but please do not let that stop you from providing feedback.

Thank you!


A note on an image component

An image component is intentionally not included in this RFC. If such a component were to be included, it would perform the following:

Users using React as part of a framework, like Next.js or Gatsby, already have access to deeply integrated image components with those features. They are widely used and well tested with a dedicated team behind them.

Users using React directly, including those using Create React App, can use Imgix's official React component, react-imgix, with the same benefits.

As such, a <PrismicImage> component is not included in the RFC. We can, however, provide proper integrations with React frameworks through gatsby-source-prismic and @prismicio/next (does not exist at the time of writing). We can also provide guidance for non-framework users.

If you feel this is not the correct approach, please comment below.

lihbr commented 2 years ago

Awesome! šŸŽ‰

I just have few comments regarding RichText and RichTextAsText:

  1. Why not prefixing them with Prismic? I feel like RichText is a common concept like Link, maybe for consistency and clarity it's better to keep the prefix (SliceZone making an exception because it's a Prismic concept?)
  2. I kinda feel weird about RichTextAsText, have we considered just having an asText boolean prop on RichText?
    
    <RichText field={doc.text} /> // Formatted

// Plain


I feel like this also kinda help with the mental modal of:
- link field > link component
- richtext field > richtext component

On a side note just had a look at the imgix integration for React, it's cooler than Vue's haha (gotcha with Vue's being that it wants you to pass just the image path, making it [not convenient to be used with Prismic's images](https://twitter.com/li_hbr/status/1309127963573727235))

Cheers!
hypervillain commented 2 years ago

Lots of good ideas. I'll take a second read next week and give a proper opinion

samlfair commented 2 years ago

Will the RichText component still accept an HTML Serializer?

By giving access to arguments, the HTML Serializer can help address edge cases. Beyond that, for advanced implementations, some users supply a higher-order function that returns an HTML Serializer to the HTML Serializer prop.

angeloashmore commented 2 years ago

@lihbr

RichText & RichTextAsText: Why not prefixing them with Prismic? I feel like RichText is a common concept like Link, maybe for consistency and clarity it's better to keep the prefix (SliceZone making an exception because it's a Prismic concept?)

I figured RichText is a Prismic concept. PrismicLink and PrismicToolbar need to be prefixed because Link and Toolbar are common, but RichText isn't.

If RichText is considered common, then yes, we should name it <PrismicRichText> and <PrismicRichTextAsText> (The AsText version is quite a lot to type now - see below).

I kinda feel weird about RichTextAsText, have we considered just having an asText boolean prop on RichText?

Yes, using different components here was intentional. A boolean prop (or an enum prop like mode="text" or mode="richText") could lead to a messy API. If mode="text" is provided for example, linkResolver and components are no longer relevant to the component. Do we type those as never now, causing a type error? Or do we accept them, but also ignore them in the implementation?

Separating Rich Text and Text into separate components keeps each component more focused. Using <Text> in itself takes the place of providing a boolean/enum prop. In other words, using the <Text> component already declares to the reader/developer that we will be rendering this RichText field as text. A prop would serve the same purpose, but with the added weirdness described above.

If we decide to prefix the Rich Text components with "Prismic", we could rename the components to be more straightforward:


@samlfair

Will the RichText component still accept an HTML Serializer?

Yes, accepting an HTML serializer should still be included. At the very least, it is for compatibility with existing codebases.

Could you give examples of edge cases that could only be handled via an HTML serializer function? I think for most cases, if not all, the components prop can handle everything if the correct data is provided to each component. If we can find a case where it cannot handle it, then htmlSerializer should continue being a first-class prop.

lihbr commented 2 years ago

I figured RichText is a Prismic concept. PrismicLink and PrismicToolbar need to be prefixed because Link and Toolbar are common, but RichText isn't.

If RichText is considered common, then yes, we should name it <PrismicRichText> and <PrismicRichTextAsText> (The AsText version is quite a lot to type now - see below).

Ok, to me RichText is definitely a common concept, not a Prismic one (+ Google for ref. https://www.google.com/search?q=richtext)

Yes, using different components here was intentional. A boolean prop (or an enum prop like mode="text" or mode="richText") could lead to a messy API. If mode="text" is provided for example, linkResolver and components are no longer relevant to the component. Do we type those as never now, causing a type error? Or do we accept them, but also ignore them in the implementation?

Separating Rich Text and Text into separate components keeps each component more focused. Using <Text> in itself takes the place of providing a boolean/enum prop. In other words, using the <Text> component already declares to the reader/developer that we will be rendering this RichText field as text. A prop would serve the same purpose, but with the added weirdness described above.

Ok, makes sense! I'm not familiar with React standards that much

If we decide to prefix the Rich Text components with "Prismic", we could rename the components to be more straightforward:

  • PrismicRichText
  • PrismicText (without the prefix, I figured a component named Text was too common, and PrismicText alongside RichText was confusing, thus RichTextAsText was selected).

I like how this sounds! <PrismicRichText /> and <PrismicText />

samlfair commented 2 years ago

Could you give examples of edge cases that could only be handled via an HTML serializer function? I think for most cases, if not all, the components prop can handle everything if the correct data is provided to each component. If we can find a case where it cannot handle it, then htmlSerializer should continue being a first-class prop.

@angeloashmore Currently, the HTML Serializer receives four arguments: type, element, content, children. Will the props argument in the component prop provide access to all of that data? If so, then there's no issue.

Otherwise, here's an edge case where we use a higher-order function for the HTML Serializer (admittedly, this is pretty obscure):

https://community.prismic.io/t/htmlserializer-grouped-images/2863

levimykel commented 2 years ago

This all looks awesome! The only suggestion I have is give the ability to choose how to join the Rich Text blocks in the < RichTextAsText /> element. Joining them with a space is probably what you need 99% of the time, but I have run into a couple instances where I've needed to join them with a new line instead.

Maybe all we need is a prop to specify that you want to join with a new line instead of a space. I'm not sure if there are any other use-cases.

angeloashmore commented 2 years ago

@lihbr

Cool! Let's go with <PrismicRichText> and <PrismicText> then.


@samlfair

The props argument may be an HTML-oriented version of the standard Rich Text serializer arguments. A link, for example, may receive the following props object:

type Props = {
  href: string
  target?: string
  rel?: string
  children: React.ReactNode
}

This is done to have greater compatibility with existing, non-Prismic-specific components. We could add a fieldData or prismic prop to all components in the components prop that contains the lower-level type, element, content, and children values.

type Props = {
  href: string
  target: string
  rel: string
  children: React.ReactNode
  prismic: {
    type: string
    element: Record<string, unknown>
    content?: React.ReactNode
    children: React.ReactNode
  }
}

That linked serializer example is pretty obscure, but shows there are cases where a function may be necessary. We can continue accepting an serializer function in these cases while recommending the object form whenever possible. We should be able to detect if a user provides a function or an object automatically.


@levimykel

Good catch! Yes, we can accept a separator prop to handle that case while defaulting to a space.

lihbr commented 2 years ago

Hey, some quick feedback I have on the refresh of the React kit after working on the refresh of the Vue kit:

Having a client instance built-in in order to provide a "simple-usage" option:

I'm unsure how relevant it is to the React ecosystem but have we considered allowing the Prismic provider to support an alternative endpoint/clientConfig option pair to the client one in order to allow a single package init of the kit? (in order to allow users to skip the install of @prismicio/client):

- import * as prismic from '@prismicio/client'
  import { PrismicProvider, PrismicLink } from '@prismicio/react'
  import { Link } from 'react-router-dom'

  import { linkResolver } from '../linkResolver'
  import { Heading } from './Heading'

- const endpoint = prismic.getEndpoint('qwerty')
- const client = prismic.createClient(endpoint)

  const richTextComponents = {
    h1: 'h2',
    h2: Heading,
    h3: (props) => <Heading as="h4" {...props} />,
  }

  const App = ({ children }) => {
    return (
      <PrismicProvider
-       client={client}
+       endpoint={"my-repo"}
+       clientConfig={{ accessToken: "xxx" }}
        linkResolver={linkResolver}
        richTextComponents={richTextComponents}
        internalLinkComponent={Link}
      >
        {children}
      </PrismicProvider>
    )
  }

The client instance could then be configured with a lazy-loaded ponyfill of a simple fetch implementation to allow seamless basic usage through SSR too.

Of course, this option would be purely additional and doesn't remove the ability to configure the provider using the client way.

My main doubt about it is that I can think of it making less sense in frameworks like Next.js where you might need to have a client instance of your own anyway for usage with getStaticProps & cie, but just wanted to suggest šŸ¤”

For reference this is something that got implemented on the new Vue kit as I couldn't think of case's like Next.js' but maybe there are cases in React where that would benefit users, maybe Vite + React, or craco šŸ¤·ā€ā™€ļø

Allowing <prismic-link /> to handle documents

You can easily end up in that scenario:

Options A: You use the routes resolver option of the API and know that the link will be available at document.url but using the routes resolver is not always possible, yet it is a bit advanced to retrieve the URL manually in the document head; Options B: You use the new helper from @prismicio/helpers: documentAsLink() and resolve each link on the template. While this work well it feels a bit like you're on your own; Options C: The <prismic-link /> component also handles documents so that this become possible:

<PrismicLink field={document} >
  {/* My beautiful blog post card */}
</PrismicLink>

It's fairly straightforward to discerns a document from a field (example)so I think this could be a nice addition šŸ¤”

Cheers!

angeloashmore commented 2 years ago

Having a client instance built-in in order to provide a "simple-usage" option

I'm conflicted here. I see the benefit of providing just client config w/ a default fetcher, but doing so directly connects the React library to the client library in a way that makes them not modular. And although we can allow users to provide a client directly, having more than one way to do the same thing could ultimately result in more confusion.

You make a good point about Next users as well - they need to use the client directly. They also would not be using any of the React library's client methods since any client work should be done in the SSR functions (getStaticProps, getStaticPaths).

I think I prefer the idea of providing a client instance rather than configuration for the client. It teaches users how to use the client library which will apply to any JavaScript project. If someone exports a client in a client.js file, it can be used in custom scripts, the React library, etc.

The client could also not be provided at all if the user does not use the client hooks. (The same would apply if we accepted the config as props - they would also be optional).

The ergonomic difference is minimal as demonstrated in your example:

- import * as prismic from '@prismicio/client'
  import { PrismicProvider, PrismicLink } from '@prismicio/react'
  import { Link } from 'react-router-dom'

  import { linkResolver } from '../linkResolver'
  import { Heading } from './Heading'

- const endpoint = prismic.getEndpoint('qwerty')
- const client = prismic.createClient(endpoint)

  const richTextComponents = {
    h1: 'h2',
    h2: Heading,
    h3: (props) => <Heading as="h4" {...props} />,
  }

  const App = ({ children }) => {
    return (
      <PrismicProvider
-       client={client}
+       endpoint={"my-repo"}
+       clientConfig={{ accessToken: "xxx" }}
        linkResolver={linkResolver}
        richTextComponents={richTextComponents}
        internalLinkComponent={Link}
      >
        {children}
      </PrismicProvider>
    )
  }

Allowing <PrismicLink /> to handle documents

This is a good idea! The component already accepts a field prop for link fields. For documents, I think it makes more sense to add an additional document prop for full documents. The component should only accept field or document (reflected in the types).

lihbr commented 2 years ago

Awesome, checks my concerns about the built-in client! Nice for links~

angeloashmore commented 2 years ago

@lihbr document is now a prop on <PrismicLink> šŸŽ‰ (https://github.com/prismicio/prismic-reactjs/commit/0768280e049d120cb93ebac581bc4e629ff98c67)

riceboyler commented 2 years ago

Just found this and it looks like a great potential step forward.

To concur with @angeloashmore, as a Next user, I'd prefer the client be independent of the Provider, though I can see where that could be useful for non SSR/SSG framework users. Would it make any sense to allow both props and if both are provided, use clientConfig over client?

angeloashmore commented 2 years ago

Hey @riceboyler, we're going to leave the client prop as is and not introduce a clientConfig option for now. This is still open for discussion if there's a good reason to support a clientConfig prop.

angeloashmore commented 2 years ago

@prismicio/react@v2.0.0 was just published (see #97) so I'm going to close this issue.

Thanks for your feedback, everyone! šŸŽ‰