Skip to content

Paciolan/remote-component

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Remote Component coverage:100%

remote-component

Load a React Component from a URL at runtime.

Table of Contents

What is a Remote Component?

A Remote Components is loaded at runtime from a URL. It is used in the same way any other React Component is used.

const url =
  "https://raw.githubusercontent.com/Paciolan/remote-component/master/examples/remote-components/HelloWorld.js";

const HelloWorld = ({ name }) => <RemoteComponent url={url} name={name} />;

const Container = (
  <>
    <HelloWorld name="Remote" />
  </>
);

Install

npm install @paciolan/remote-component

Dependencies

The React Application and Remote Component can share dependencies. The dependencies must be configured explicitly.

Shared dependencies in the Remote Component must be marked as external so they are not bundled in the output.

All shared dependencies must be provided by the React Application.

Injecting Dependencies with Webpack

Create a file in the root of your React Application called remote-component.config.js. Some frameworks like Create React App (CRA) might need this file placed inside the src directory. The location can be changed inside of webpack.config.js.

This file will supply the Remote Components with their needed external dependencies.

/**
 * Dependencies for Remote Components
 */
module.exports = {
  resolve: {
    react: require("react")
  }
};

Add a Webpack alias inside of webpack.config.js so the RemoteComponent can load this file.

module.exports = {
  resolve: {
    alias: {
      "remote-component.config.js": __dirname + "/remote-component.config.js"
    }
  }
};

Injecting Dependencies without Webpack

Projects without webpack can still use a Remote Component through a manual configuration.

Follow the directions in Injecting Dependencies with Webpack to create the remote-component.config.js.

Create src/RemoteComponent.js and import the dependencies from remote-component.config.js.

import {
  createRemoteComponent,
  createRequires
} from "@paciolan/remote-component";
import { resolve } from "../remote-component.config.js";

const requires = createRequires(resolve);

export const RemoteComponent = createRemoteComponent({ requires });

Then you will change the import for RemoteComponent to point to this new file.

import { RemoteComponent } from "./RemoteComponent";

Custom Fetcher

The Custom Fetcher is a feature for advanced users only. It exposes the fetcher from the underlying @paciolan/remote-module-loader.

Refer to @paciolan/remote-module-loader documentation for more information about how to use the fetcher.

const fetcher = url => axios.get(url).then(request => request.data);

fetchRemoteComponent({ url, requires, fetcher });

Adding a Remote Component to a React App

Import RemoteComponent from either @paciolan/remote-component or your custom ./src/RemoteComponent.js (depending on your setup).

It is recommended to wrap <RemoteComponent /> in a component for better naming and separation. This is optional.

Pass the url to the <RemoteComponent />.

Use a RemoteComponent like a regular React Component.

import React from "react";
import ReactDOM from "react-dom";
import { RemoteComponent } from "@paciolan/remote-component";

const element = document.getElementById("app");
const url = "https://raw.githubusercontent.com/Paciolan/remote-component/master/examples/remote-components/HelloWorld.js"; // prettier-ignore

const HelloWorld = props => <RemoteComponent url={url} {...props} />;

ReactDOM.render(<HelloWorld name="Paciolan" />, element);

Render Props

In the case you need more control over the error or rendering, you can use a render prop.

const HelloWorld = props =>
  <RemoteComponent
    url={url}
    render={({ err, Component }) =>
      err ? <div>{err.toString()}</div> : <Component {...props} />
    }
  />
);

React Hooks

If you need even more control, you can create a custom useRemoteComponent React Hook.

Start by creating src/useRemoteComponent.js.

import {
  createRequires,
  createUseRemoteComponent
} from "@paciolan/remote-component";
import { resolve } from "../../remote-component.config.js";

const requires = createRequires(resolve);

export const useRemoteComponent = createUseRemoteComponent({ requires });

Next, use the custom hook.

import { useRemoteComponent } from "./useRemoteComponent";

const url = "https://raw.githubusercontent.com/Paciolan/remote-component/master/examples/remote-components/HelloWorld.js"; // prettier-ignore

const HelloWorld = props => {
  const [loading, err, Component] = useRemoteComponent(url);
  
  if (loading) {
    return <div>Loading...</div>;
  }

  if (err != null) {
    return <div>Unknown Error: {err.toString()}</div>;
  }

  return <Component {...props} />;
};

Named import example

import { useRemoteComponent } from "./useRemoteComponent";

const url = "https://raw.githubusercontent.com/Paciolan/remote-component/master/examples/remote-components/HelloWorld.js"; // prettier-ignore

const HelloWorld = props => {
  const [loading, err, Component] = useRemoteComponent(url, "customImportName");

  if (loading) {
    return <div>Loading...</div>;
  }

  if (err != null) {
    return <div>Unknown Error: {err.toString()}</div>;
  }

  return <Component {...props} />;
};

Creating a Remote Component

Creating a Remote Component involves creating a CommonJS module. That module should have react and other shared dependencies excluded from the bundle. It should also already be transpiled for browser support.

Remote Component Starter Kit

Clone the remote-component-starter for a ready to go project.

Creating a Remote Component with Webpack

The Remote Component must be exported.

import React from "react";

const RemoteComponent = () => {
  return <div>Hello Remote World!</div>;
};

export default RemoteComponent;

Inside of the webpack.config.js, the libraryTarget must be set to commonjs.

Any shared dependencies must be added as an external. This will prevent them from being bundled in the library.

module.exports = {
  output: {
    libraryTarget: "commonjs"
  },
  externals: {
    react: "react"
  }
};

Inside of the package.json, set main to the webpack entrypoint. This will probably be dist/main.js.

Shared dependencies you have marked as external should be removed from dependencies and added to both devDependencies (so they are available during development) and peerDependencies (so the upstream package knows it is responsible for installation).

The dependency version should match the version inside the React Application.

{
  "main": "dist/main.js",
  "devDependencies": {
    "react": "^16.8"
  },
  "peerDependencies": {
    "react": "^16.8"
  }
}

Create React App (CRA)

Start a new Create React App app or use an existing one. This is a React Application that will import Remote Components.

$ npx create-react-app my-react-app
$ cd my-react-app

Create src/remote-component.config.js. note: CRA requires this file to be placed inside of src.

/**
 * These dependencies will be made available to the Remote Components.
 */
module.exports = {
  resolve: {
    react: require("react")
  }
};

Create src/RemoteComponent.js. The RemoteComponent must manually be created because CRA does not provide access to webpack.config.js without ejection.

import {
  createRemoteComponent,
  createRequires
} from "@paciolan/remote-component";
import { resolve } from "./remote-component.config.js";

const requires = createRequires(resolve);

export const RemoteComponent = createRemoteComponent({ requires });

You may see the following warning. It is safe to ignore.

Compiled with warnings.

./node_modules/@paciolan/remote-component/dist/getDependencies.js
Module not found: Can't resolve 'remote-component.config.js' in '/et/repo/cra-remote-component/node_modules/@paciolan/remote-component/dist'

You can get rid of this warning by directly including the files. This is unsafe as these files paths could change.

import { createRemoteComponent } from "@paciolan/remote-component/dist/createRemoteComponent";
import { createRequires } from "@paciolan/remote-component/dist/createRequires";
import { resolve } from "./remote-component.config.js";

const requires = createRequires(resolve);

export const RemoteComponent = createRemoteComponent({ requires });

Server Side Rendering with Next.js

Server Side Rendering with Next.js is currently (EXPERIMENTAL).

Follow the steps in Injecting Dependencies with Webpack to create the remote-component.config.js.

getServerSideProps

Add a getServerSideProps method to your Remote Component. This follows the Next.js pattern.

import React from "react";

const Person = ({ data }) => {
  const entries = Object.entries(data);
  const rows = entries.map(([key, value], i) => (
    <tr>
      <th>{key}</th>
      <td>{value}</td>
    </tr>
  ));

  return <table>{rows}</table>;
};

const getServerSideProps = async ({ data }) => {
  const response = await fetch(`https://swapi.dev/api/people/${data.id}`);
  return await response.json();
};

Person.getServerSideProps = getServerSideProps;

export default Person;

Calling getServerSideProps from Next.js

Modify the Next.js page that will contain the Remote Component.

Add these imports. Notice how getServerSideProps is renamed to getProps to prevent conflicts with the Next.js function of the same name.

import {
  createRequires,
  fetchRemoteComponent,
  getServerSideProps as getProps
} from "@paciolan/remote-component";
import dynamic from "next/dynamic";
import config from "../remote-component.config";

Create the requires for shared dependencies that will be provided to the Remote Component. Then pass url and requires into fetchRemoteComponent. Wrap this inside of dynamic.

const requires = createRequires(config.resolve);

const url = "http://localhost:5000/MyRemoteComponent.js";
const MyRemoteComponent = dynamic(() =>
  fetchRemoteComponent({ url, requires })
);

Create Next.js's getServerSideProps function. Pass the Next.js context (if the component needs the context) as well as any data in as context when calling getProps.

export async function getServerSideProps(context) {
  const data = { id: 1 };
  const myData = await getProps({
    url,
    requires,
    context: { ...context, data }
  });
  return { props: { myData } };
}

The props returned from Next.js's getServerSideProps function will be passed into the props. You can then use those props to send the data into the Remote Component.

export default function MyPage(props) {
  return (
    <div>
      <MyRemoteComponent data={props.myData} />
    </div>
  );
}

How it works

The RemoteComponent React Component takes a url as a prop. The url is loaded and evaluated. This file must be a valid CommonJS Module that exports the component as default.

While the url is loading, the fallback will be rendered. This is a similar pattern to React.Suspense. If no fallback is provided, then nothing will be rendered while loading.

Once loaded, there will either be an err or a Component. The rendering will first be handled by the render callback function. If there is no render callback and err exists, a generic message will be shown.

The Component will be rendered either to the render callback if one exists, otherwise it will be rendered as a standard component.

Content Security Policy (CSP)

Sites with a content_security_policy header set are likely to not work. CSP puts a restriction on using new Function, which remote-module-loader relies upon.

This library depends on @paciolan/remote-module-loader, which does not support CSP. Until CSP is supported in @paciolan/remote-module-loader, it cannot be supported.

Read more on CSP

Alternatives

Roadmap

  • Add support for multiple components import from a single URL.
  • Add TypeScript support

Caveats

There are a few things to be aware of when using RemoteComponent.

  • Calls to a RemoteComponent add an additional HTTP call.
  • Dependencies could be included twice. If a dependency is included in the library and also in the Web App. This could have unknown effects.
  • The external dependencies of the library and Web Application must match. This makes upgrading 3rd party libraries that have breaking changes more complex.
  • The RemoteComponent and web application's browser targets must match. If your React App targets IE11, but the Remote Component does not, then it will not work in IE11.
  • Debugging could be more complicated as source map support does not (yet) exist.
  • Nested RemoteComponents can get exponentially hard to manage (dependencies) and develop (running multiple repositories at the same time for localhost)
  • Content Security Policy (CSP) is not supported.

Contributors

Joel Thoms (https://twitter.com/joelnet)

Icons made by smalllikeart from www.flaticon.com

Icons made by turkkub from www.flaticon.com

Icons made by Freepik from www.flaticon.com