@sitecore/byoc
v0.2.15
Published
Bring-Your-Own-Components runtime to register and retrieve react components
Downloads
82,763
Keywords
Readme
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:
- Bundle component that lists all BYOC components that need to be a part of clientside js - note that it uses a special kind of import “for side-effects”, that ensures that code will be a part of the bundle regardless of if it’s used or not.
- A place where this bunde is included
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.
// Somewhere in the user app
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:
// keep the side-effect import to ensure the unconditional bundling
import '@sitecore/components/form'
// destructure as a separate import
import {Form} '@sitecore/components/form'
// use it in the app
;<Form formId='my-form-id' />
Example of rendering a component through a wrapper:
// import the necessary component
import '@sitecore/components/form'
// import BYOC runtime
import * as BYOC from '@sitecore-feaas/byoc'
// use it in the app
;<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
BYDO allows developers to register datasource definitions to be used in Pages and Components app. Datasources defined this way will act the same way as regular datasources created in Components Builder UI. In addition, it is possible to use BYOD mechanism to intercept and customize datasources created in the UI.
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
package:
import { registerDatasource } from '@sitecore/byoc'
Then, you can use the registerDatasource
function to register a data source. The function takes two arguments:
handler
: A function that customizes the settings of the data source when called from aRegisteredDatasource
. This function can return either DataSettings for HTTP request or a Promise to return the data.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` directly
Example 1: HTTP-based datasource, described by schema
Handler function returns DataSettings detailing the fetch request. The options object is a JSON schema describing the response.
registerDatasource(
() => ({
// DataSettings is `url`, `headers`, `params`, `method` and `body`.
url: 'https://api.sampleapis.com/wines/reds'
}),
{
id: 'http-and-schema',
name: 'Wines via HTTP',
description: 'List of red wines fetched by HTTP, each with `wine`, `price` and `id` property',
// options object is JSON-schema compatible
type: 'array',
properties: {
wine: { type: 'string' },
price: { type: 'string' },
id: { type: 'number' }
}
}
)
Example 2: Customizing data source settings
When registerDatasource
is given an ID of a datasource that exist in the library (i.e. created via UI), the handler
can adjust the data settings of the original request.
registerDatasource(
// settings will contain DataSettings as specified via UI in Components app
(settings) => ({
...settings,
params: {
// add ?page=2 parameter to the original URL
...settings.params,
page: 2
},
headers: {
// add Authorization header in addition to original headers
...settings.headers,
Authorization: 'Bearer token'
}
}),
{
// ID of a datasource as created in UI (can be visible in the address bar URL) to be extended
// No other options need to be specified for this case of intercepting datasource.
id: 'aBcDaaa23a'
}
)
Example 3: Reading JSON from file, with sample data.
When handler returns promise (e.g. if it's async function), the promised data is passed to component directly bypassing the original HTTP request. This allows returning data through alternative means, like reading from a file, database, etc.
import { promises as fs } from 'fs'
// Async handlers supposed to return data itself instead of DataSettings
registerDatasource(
async () => {
return JSON.parse(await fs.readFile('wines.json', 'utf-8'))
},
{
id: 'file-and-sample',
name: 'Wines from JSON file',
description: 'JSON file read and parsed from file (no HTTP request is made), with sample data',
// Instead of JSON schema, sample data can be provided directly.
sample: [
{ wine: 'Emporda 2012', id: 1, price: '$250' },
{ wine: 'Pêra-Manca Tinto 1990', id: 2, price: '$312' }
]
}
)
More on schema
and sample
options
- if a
sample
was provided in the options it assigns it to the sample property ofRegisteredDatasource
as is. The user will be able to map their components against that sample data. - if a
schema
was provided in the options argument ofregisterDatasource
it assigns it to theschema
property of theRegisteredDatasource
, so that the UI can generate sample automatically if it's not provided. - If a
properties
object was provided in the options ofregisterDatasource
, it's added to theschema
of theRegisteredDatasource
- 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.