Sitecore BYOC components
A repository of react components authored by Sitecore that offers integration of different products in an
easy-to-consume package. The promise of it is that it's mostly plug-and-play business.
What is BYOC?
BYOC is a way to register react components into Pages/Components app from within User app. It’s a streamlined system
that makes it easy to add functionality in a way that is familiar to the regular developer.
Using BYOC requires two steps:
- Defining a react component (regular everyday react component, not sxa)
- Registering that component (using BYOC.registerComponent call).
Use cases
BYOC components serve two audiences, and solve two problems:
User own components
In this the component is defined and registered from within the User app
(example).
Sitecore does not really have access to the source and definition of those components, and yet they appear in the UI as
if they were native. It’s really good way for the users to extend their Sitecore websites, as they can use any tools,
dependencies and techniques. They can choose to re-use their components, or create one-offs specific to the app. It goes
as far as supporting hot-reloading their code right in the browser within the context of developer’s next.js render
host.
Sitecore components
The other large use case for BYOC is to allow Sitecore products to be integrated together easily, using a shared
mechanism allows making changes easier. It shortens the release cycle, and de-risks the new additions to the ecosystem.
Unlike user components, the sitecore components are imported from an npm package. New components become an opt-in to the
user app, the user only needs to update the version of the npm package, and import what they need in their app.
Modes of operation
BYOC components attempt to offer solutions for most of the developer needs, beyond what’s typically expected.
Essentially it’s all different combinations of rendering component on server or client-side.
Rendered on client
The basic use case is a component that adds interactivity on the page, such as smart input field, or a tool to display
some dynamic content (e.g. sitecore forms). In this case, the component has to be loaded on the clientside. In next.js
it may not be so straighforward, as it tends to optimize out the components that aren’t used. Additionally, in app
router setup of next.js 13, all components are by default considered server side, so clientside components need to be
opt-in. The solution to this is to define a specia bundle component that lists all the components that are expected to
work on clientside, and then placing that component into the layout of an app. This ensures that the code is loaded to
the client correctly. See example:
Rendered on server
Other class of components is rendered on server. This allows the output of the component be indexable by search engines,
and enables fully static websites that have very little clientside javascript.
There are two variations of these:
- Old-style server component - a one that can not use hooks like useEffect, but can output JSX. All the dynamic
data fetching must happen on the page level. This is an approach JSS is taking for BYOC components.
- New async server components - a type of server-only component that can have its own asynchronous logic (e.g.
fetching data, calling apis, etc), making it very powerful and easy to use. It is a great way to mak make secure
requests to databases, services and apis without exposing the secrets and credentials to the clientside app. The
downside is that it requires next.js 13 app router style of application. This will not be available in JSS apps for
now.
Example
Server components need to be imported somewhere in the app,
e.g. in layout.
Notice that it’s a side-effect import (i.e. it does not destructure the exports). This is a special way to import that
opts the code out of tree-shaking. This is important, as next.js app does not know which components are used in the
layout tree or feaas component, so it should not try to optimize them away.
Hybrid component
A component that renders on server and later gets hydrated on the clientside is pretty popular too. It allows the page
to be indexable, and it makes user see parts of the design before app is fully initialized. It’s a good idea to try to
use this style of components wherever possible. For this to work, the component needs to be imported both in
client side
and
server side
of the next.js bundle.
Example
Swappable component
Another way to combine two components is that server side renders something different from the clientside. It could be a
placeholder, or empty state of a component, that gets replaced with a interactive one as soon as the page loads. Since
it’s a variation of hybrid omponent, it also requires registering 2 components, one on the server and one on the
clientside.
Example
Wrapper component
In apps that support app router and async components, it is possible to create combinations of async and sync
components. For
example
async server component fetches data and passes it to clientside component to do something with it. Since there’s server
and client component involved, it requires each of those to be registered on server and in client side bundles
separately.
Guidelines
1. Each component must be exported individually:
This is so users can choose what they want to use.
import '@sitecore/components/form'
import '@sitecore/components/search'
Keep in mind this special side effects
import syntax, which opts out of code tree-shaking. It means the components
will be included in the user app, regardless of if they are used in the page or not. The reason for this is that XM
and FEAAS components both are rendered dynamically, so the tree-shaking algorithm can not possibly do a good job.
2. There's no bundling, only typescript transpilation
This is to avoid double bundling of different versions of the shared libraries. Adding dependencies to the package is
OK. All of the dependencies will be installed into the user app, but they wont be bundled into their code unless
component is actually used. This is why it is important
Keep dependencies to the minimum to avoid bloat.
3. Components can be rendered directly
Usually BYOC components are rendered as a part of XM page or FEAAS component, so there's no need to refer to them
directly. But if there's a need to render a component manually (e.g. in Layout of an app), it can be possible to render
the the components directly. However it's important to keep the side-effect import in place.
Example of rendering a component directly:
import '@sitecore/components/form'
import {Form} '@sitecore/components/form'
;<Form formId='my-form-id' />
Example of rendering a component through a wrapper:
import '@sitecore/components/form'
import * as BYOC from '@sitecore-feaas/byoc'
;<BYOC.Component componentName='form' formId='my-form-id' />
Authoring
See
@sitecore-feaas/clientside
readme for more information about component registration under Bring your own components section.
BYOD(Bring your own data) - registerDatasource
The registerDatasource
function is a key part of the @sitecore/byoc
package. It allows you to register a data source
that can be used by your application.
Usage
First, import the registerDatasource
function from the @sitecore/byoc/data
package:
import { registerDatasource } from '@sitecore/byoc'
Then, you can use the registerDatasource
function to register a data source. The function takes two arguments:
resolver
: A function that customizes the settings of the data source when called from a RegisteredDatasource
. This
function can return either DataSettings or a Promise.options: DatasourceOptionsInput
: An object with the following properties:
id
: A unique identifier for the data source.description
: (Optional) A textual description to be displayed in UIname
: (Optional) The name of the data source.schema
: (Optional) The JSON schema for the data source.sample
: (Optional) Sample data for the data source.properties: (Optional) Alternative way to provide schema is to provide
properties` :
Resolver
The resolver
function is a key part of the registerDatasource
function. It allows you to customize the settings of
the data source. This function can return either DataSettings
or a Promise
. The resolver
function accepts a
settings
parameter of type DataSettings
. These settings are used to fetch data for a component in the client-side
module. Essentially, these are fetch
options with a url
. For instance, you can provide an Authorization
header by
returning adjusted settings from the resolver with the specified header.
If a Promise
is returned from the resolver
, the original request is not actually made. This allows providing data
from sources other than HTTP (e.g., reading from a file).
const dataResolveSettings: DataResolver = (settings) => ({
url: 'http://my-url.com',
headers: {
Authorization: 'Bearer token'
}
})
const dataResolverPromise: DataResolver = (settings) =>
Promise.resolve({
hello: 'world'
})
Registering datasource
Typically registerDatasource
function is used to create a new data source with a unique id
. The function takes a
dataResolver
and an options
object as parameters. The options
object should include the id
, schema
or sample
properties.
registerDatasource(dataResolver, {
id: 'datasource-id',
name: 'Datasource based on schema',
mode: 'register',
title: 'title',
schema: {
type: 'object',
properties: {
name: { type: 'string', description: 'First and Last name' },
age: { type: 'number' }
}
}
})
Extending datasources
It is possible to extend datasource that is already present in the Builder App by refering to its id. In that case
schema or sample dont need to be provided. When data is being fetched, the resolver function will be called allowing the
developer to intercept or customize data request. If no datasource is found with the given ID, it will not be displayed.
registerDatasource(() => ({
...settings,
header: {
...settings.header,
Authorization: 'Bearer token'
}
}), { id: 'existing-datasource-id' })
In this example, the data source with the id existing-datasource-id
will be extended with the anotherResolver
.
How It Works
When you call registerDatasource
, the data source is added to a registry of data sources. If the code is running in an
iframe and the parent window is different from the current window, the data sources are sent to the parent window. This
is done using the window.postMessage
method, which allows scripts to safely pass messages between windows or iframes,
even if they have different origins.
The data sources are of type RegisteredDatasource
and stored in the window.BYOCDatasources
object, which is declared
in the global scope. This can be used for inspection. Datasources that are registered in server context will also be
seen in that registry, although their settings will have to be serialized into the DOM to be read on the clientside.
RegisteredDatasource
The registerDatasource
function creates RegisteredDatasource
objects. The RegisteredDatasource
type is defined as
follows:
export interface RegisteredDatasource {
id: string
name: string
schema?: JSONSchema
sample?: any
resolver?: DataResolver
}
Here are the properties of a RegisteredDatasource
:
id
: A unique identifier for the data source.name
: The name of the data source.schema
: An optional JSON schema associated with the data source.sample
: An optional property that can hold any sample data associated with the data source.resolver
: A function that customizes DataSettings.
More on schema
and sample
options
- if a
sample
was provided in the options it assigns it to the sample property of RegisteredDatasource
as is. The
user will be able to map their components against that sample data. - if a
schema
was provided in the options argument of registerDatasource
it assigns it to the schema
property of
the RegisteredDatasource
, so that the UI can generate sample automatically if it's not provided. - If a
properties
object was provided in the options of registerDatasource
, it's added to the schema
of the
RegisteredDatasource
- Finally, if a title was provided in the datasourceOptions, it's added to the schema of the
RegisteredDatasource
. - If neither sample or schema were provided, the datasource is considered to be in extension mode and will not be
displayed in UI by itself.