1.0.0 • Published 2 months ago

@remote-dom/react v1.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
2 months ago

@remote-dom/react

Utilities for rendering Remote DOM elements using React.

Installation

npm install @remote-dom/core @remote-dom/react --save # npm
pnpm install @remote-dom/core @remote-dom/react --save # pnpm
yarn add @remote-dom/core @remote-dom/react # yarn

Usage

This library provides helpers for both the “host” and “remote” environments.

The @remote-dom/react entry provides wrappers for RemoteElement subclasses that add some nice React-specific features on top of core Remote DOM library. Additionally, React DOM checks for a few browser globals that are not covered in the core DOM polyfill. If you are using React in a Web Worker, you will also need to import @remote-dom/react/polyfill before react-dom to ensure that these globals are available.

The @remote-dom/react/host provides the RemoteReceiver class and RemoteRootRenderer component, which are used to render a tree of remote elements to the host environment using React.

Remote

Polyfill

Some versions of React DOM check for a few specific browser globals on initialization. If you are polyfilling the DOM using @remote-dom/core/polyfill, you will also need to import @remote-dom/react/polyfill before react-dom to ensure that these globals are available.

import '@remote-dom/core/polyfill';
import '@remote-dom/react/polyfill';

import {createRoot} from 'react-dom/client';

// Render your React app, as shown in the following examples.

createRemoteComponent()

As of version 18, React has minimal support for custom elements. To make the React integration a bit more seamless, Remote DOM provides the createRemoteComponent() function to create a React wrapper component around a custom element. This wrapper component will automatically have the TypeScript prop types it should, given the custom element definition you pass in. It will also ensure React props are passed along as element properties, and will convert any React elements passed as props to slotted children.

For example, imagine a ui-card custom element that takes a header slot, which should be used on a ui-heading custom element:

import {RemoteElement} from '@remote-dom/core/elements';

class Card extends RemoteElement {
  static get remoteSlots() {
    return ['header'];
  }
}

class Heading extends RemoteElement {}

customElements.define('ui-card', Card);
customElements.define('ui-heading', Heading);

The createRemoteComponent() wrapper will allow you to pass a header prop to the resulting React component, which can be any other React element:

import {createRoot} from 'react-dom/client';
import {createRemoteComponent} from '@remote-dom/react';

const Card = createRemoteComponent('ui-card', CardElement);
const Heading = createRemoteComponent('ui-heading', HeadingElement);

createRoot(document.querySelector('#root')).render(
  <Card header={<Heading>Hello, world!</Heading>}>
    This is the body of the card.
  </Card>,
);

To make this work, the wrapper component will wrap the React element prop in a <remote-fragment> element, and place it as a child of the parent element. So, the above example could have been written manually like this:

import {createRoot} from 'react-dom/client';

createRoot(document.querySelector('#root')).render(
  <ui-card>
    This is the body of the card.
    <remote-fragment slot="header">
      <ui-heading>Hello, world!</ui-heading>
    </remote-fragment>
  </ui-card>,
);

To disable this behavior, you can pass {slotProps: {wrapper: false}} option to createRemoteComponent(). This will cause any element props to be cloned with a slot prop, instead of wrapping them in a <remote-fragment> element.

import {createRoot} from 'react-dom/client';
import {createRemoteComponent} from '@remote-dom/react';

const Card = createRemoteComponent('ui-card', CardElement, {
  slotProps: {
    wrapper: false,
  },
});

const Heading = createRemoteComponent('ui-heading', HeadingElement);

createRoot(document.querySelector('#root')).render(
  <Card header={<Heading>Hello, world!</Heading>}>
    This is the body of the card.
  </Card>,
);

// Now, renders this tree of HTML elements:
// <ui-card>
//   This is the body of the card.
//   <ui-heading slot="header">Hello, world!</ui-heading>
// </ui-card>

Host

RemoteReceiver

The @remote-dom/react/host package re-exports the RemoteReceiver class from @remote-dom/core. This object will store the state of the remote tree of elements, and the RemoteRootRenderer component expects to receive an instance of this class in order to map the remote tree to React components.

createRemoteComponentRenderer()

The RemoteRootRenderer component needs a map of which React components to render for each remote element. These components will receive a description of the remote element, but not much more. The createRemoteComponentRenderer() function can be used to create a wrapper React component that will automatically update whenever the properties or children of the associated remote element change. It will also provide some helpful transformations, like mapping child elements with slot attributes into props.

import {createRemoteComponentRenderer} from '@remote-dom/react/host';

// Imagine we are implementing the host version of our `ui-card` custom element above,
// which allows a `header` slot. We’ll also have it accept a `subdued` property to
// customize its appearance.

const Card = createRemoteComponentRenderer(function Card({
  header,
  subdued,
  children,
}) {
  return (
    <div class={['Card', subdued && 'Card--subdued'].filter(Boolean).join(' ')}>
      {header && <div class="Card__Header">{header}</div>}
      {children}
    </div>
  );
});

RemoteRootRenderer

The RemoteRootRenderer component is used to render a tree of remote elements to the host environment using React. It expects a receiver prop, a RemoteReceiver instance that will store the state of the remote tree of elements. It also accepts a components prop, which provides the mapping of which React component to render for each remote element.

The following component shows an example of how you could render a tree of remote elements using React. You’ll need to hand the receiver object’s connection property to the remote environment; some examples of how to do this are shown in the runnable Remote DOM examples.

import {createRoot} from 'react-dom/client';
import {
  createRemoteComponentRenderer,
  RemoteRootRenderer,
  RemoteReceiver,
} from '@remote-dom/react/host';

// Create wrapper elements to render our actual UI components in response
// to remote elements. See the `createRemoteComponentRenderer()` section above.
const Card = createRemoteComponentRenderer(UICard);

const receiver = new RemoteReceiver();
// TODO: send the `receiver.connection` object to the remote environment,
// so it can send us updates about the tree of remote elements.

createRoot(document.querySelector('#root')).render(
  <RemoteRootRenderer
    receiver={receiver}
    components={new Map([['ui-card', Card]])}
  />,
);

RemoteFragmentRenderer

As noted above, Remote DOM may render a <remote-fragment> element to wrap a React element that is passed as a prop to a remote element. The RemoteFragmentRenderer component is used to render these fragments — it simply renders each child element inside of a React wrapper, which prevents any unnecessary wrapper elements from being introduced in the final DOM output.

import {createRoot} from 'react-dom/client';
import {
  createRemoteComponentRenderer,
  RemoteRootRenderer,
  RemoteFragmentRenderer,
  SignalRemoteReceiver,
} from '@remote-dom/react/host';

// Same setup as above...

createRoot(document.querySelector('#root')).render(
  <RemoteRootRenderer
    receiver={receiver}
    components={
      new Map([
        ['ui-card', Card],
        // We allow `remote-fragment` elements to be rendered, which is the
        // name Remote DOM gives these wrapper elements by default. If you changed
        // the name using the `slotProps.wrapper` option, match that name here.
        ['remote-fragment', RemoteFragmentRenderer],
      ])
    }
  />,
  document.querySelector('#root'),
);
1.0.0

2 months ago

0.1.2

2 months ago

0.1.1

2 months ago

0.1.3

2 months ago

0.1.0

2 months ago

0.0.2

2 months ago

0.0.1

2 months ago