Sharing React Query across micro frontends with single-spa and webpack module federation

  • Using customProps where a host app instantiates the QueryClient and passes it to the micro frontends
  • Using a bi-directional host where a host app instantiates the QueryClient and exposes it to micro frontends using module federation

Sharing ReactQuery using customProps

├── packages
│ ├── host
│ ├── app1
│ ├── app2
│ └── shared
  • host: the single-spa root config that contains the html file that is shared by all the micro frontends. It is responsible for booting up the micro frontends and need be, mapping them to individual routes.
  • app1: a micro frontend
  • app2: another micro frontend. It is nearly identical to app1
  • shared: a component library containing a component named Example that uses useQuery to fetch data.
shared
├── index.html
├── package.json
├── rollup.config.js
└── src
├── Example.jsx
└── index.js
// Example.jsximport React from "react";
import { useQuery } from "react-query";
function Example() {
const [show, setShow] = React.useState(false);
return (
<div>
<div>
<button onClick={() => setShow(!show)}>Show/Hide</button>
</div>
{show ? <RepoData /> : null}
</div>
);
}
const queryOptions = {
staleTime: Infinity,
};
function RepoData() {
const { isLoading, error, data, isFetching } = useQuery(
"repoData",
() =>
fetch(
"https://api.github.com/repos/tannerlinsley/react-query"
).then((res) => res.json()),
queryOptions
);
if (isLoading) {
return "Loading...";
}
if (error) {
return "An error has occurred: " + error.message;
}
return (
<>
<h3>{data.name}</h3>
<p>{data.description}</p>
<strong>👀 {data.subscribers_count}</strong>{" "}
<strong>✨ {data.stargazers_count}</strong>{" "}
<strong>🍴 {data.forks_count}</strong>
<div>{isFetching ? "Updating..." : ""}</div>
</>
);
}
// rollup.config.jsimport babel from "@rollup/plugin-babel";
import resolve from "@rollup/plugin-node-resolve";
import pkg from "./package.json";export default {
input: "src/index.js",
plugins: [
resolve({
extensions: [".js", ".jsx"],
}),
babel({
babelHelpers: "runtime",
plugins: ["@babel/plugin-transform-runtime"],
presets: ["@babel/preset-react"],
}),
],
output: [
{
file: pkg.module,
format: "esm",
sourcemap: true,
},
],
external: ["react", "react-dom", "react-query"],
};
app1
└── src
├── App.jsx
├── index.js
├── main.jsx
└── singleSpaEntry.js
  • App.jsx is the top most component of this app, it imports the Example component from ‘shared’, and sets up the QueryClientProvider. Note that it receives the QueryClient instance as a prop, this is important!
import React from "react";
import { QueryClientProvider } from "react-query";
import { Example } from "@demo/shared";
function App({ title = "", queryClient }) {
return (
<QueryClientProvider client={queryClient}>
<h2>{title}</h2>
<Example />
</QueryClientProvider>
);
}
// main.jsximport React from "react";
import ReactDOM from "react-dom";
import { QueryClient } from "react-query";
import App from "./App";const c = new QueryClient();ReactDOM.render(<App queryClient={c} />, document.getElementById("app"));
// main.jsimport("./main");
export {};
// singleSpaEntry.jsimport React from "react";
import ReactDOM from "react-dom";
import singleSpaReact from "single-spa-react";
import App from "./App";const appLifeCycles = singleSpaReact({
React,
ReactDOM,
rootComponent: App,
errorBoundary() {
// https://reactjs.org/docs/error-boundaries.html
return <div>This renders when a catastrophic error occurs</div>;
},
});
export function bootstrap(props) {
return appLifeCycles.bootstrap(props);
}
export function mount(props) {
return appLifeCycles.mount(props);
}
export function unmount(props) {
return appLifeCycles.unmount(props);
}
// webpack.config.jsconst path = require("path");const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
mode: "production",
entry: {
app: path.join(__dirname, "src", "index.js"),
},
output: {
filename: "[name].[chunkhash].bundle.js",
path: path.resolve(__dirname, "dist"),
clean: true,
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: "babel-loader",
exclude: /node_modules/,
options: {
presets: ["@babel/preset-react"],
},
},
],
},
resolve: {
extensions: ["*", ".js", ".jsx"],
modules: ["node_modules", path.resolve(__dirname, "src")],
},
devServer: {
port: "3001",
hot: false,
},
devtool: "source-map",
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, "public", "index.html"),
}),
new ModuleFederationPlugin({
name: "app1",
filename: "remoteEntry.js",
exposes: {
"./App": "./src/singleSpaEntry",
},
shared: [
//
"react-dom",
"react-query",
"react",
"single-spa-react",
],
}),
],
};
"scripts": {
"start": "webpack serve --mode development",
"build": "webpack",
"serve": "serve dist -p 3001"
}
// src/index.js
import("./register");
// src/register.js
import { registerApplication, start } from "single-spa";
import { QueryClient } from "react-query";const queryClient = new QueryClient();registerApplication({
name: "app1",
app: () => import("app1/App"),
activeWhen: "/",
customProps: {
title: "App 1 running on host",
queryClient,
},
});
registerApplication({
name: "app2",
app: () => import("app2/App"),
activeWhen: "/",
customProps: {
title: "App 2 running on host",
queryClient,
},
});
start();
  • shared exports the Example component which uses React Query to make a network call.
  • App1 and App2 import the Example component from shared, and are exposed as single-spa applications using module federation.
  • host: a single-spa host applications which loads separate micro frontends using module federation. Each separate micro frontend receives the same instead of ReactQueryClient in order to share the same data cache.

Using a bi-directional host

  • App1 and App2
// AppRemote.jsximport React from "react";
import App from "./App";
import queryClient from "host/queryClient";function AppRemote(props) {
return <App {...props} queryClient={queryClient} />;
}
export default AppRemote;
// singleSpaEntry.js
import App from "./AppRemote";
const appLifeCycles = singleSpaReact({
React,
ReactDOM,
rootComponent: App,
errorBoundary() {
// https://reactjs.org/docs/error-boundaries.html
return <div>This renders when a catastrophic error occurs</div>;
},
});
// packages/app1/webpack.config.js
// packages/app2/webpack.config.js
new ModuleFederationPlugin({
name: "app2",
filename: "remoteEntry.js",
exposes: {
"./App": "./src/singleSpaEntry",
},
remotes: {
host: "host@http://localhost:3000/remoteEntry.js",
},
shared: [
//
"react-dom",
"react-query",
"react",
"single-spa-react",
],
}),
// packages/host/webpack.config.jsnew ModuleFederationPlugin({
name: "host",
remotes: {
app1: "app1@http://localhost:3001/remoteEntry.js",
app2: "app2@http://localhost:3002/remoteEntry.js",
},
exposes: {
"./queryClient": "./src/queryClient",
},
filename: "remoteEntry.js",
shared: ["react-query"],
}),
// packages/host/src/queryClientimport { QueryClient } from "react-query";const queryClient = new QueryClient();export default queryClient;

--

--

--

Previously Paris and Geneva, currently New York. Can also be found at https://dev.to/arnaud

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

An Overview of ReactJS: Why Experts Prefer it in Web Development?

Unicode Character problems in JSON and playing with BOM (Byte Order Mark)

React Native Component Part 2

RecyclerListView: High performance ListView for React Native and Web

Query vs. URL Parameters in Express.js

Working with React- behind the scene

A quick guide to help you understand and create ReactJS apps

GraphQL on top of Spotify API = 🎆

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Arnaud Dostes

Arnaud Dostes

Previously Paris and Geneva, currently New York. Can also be found at https://dev.to/arnaud

More from Medium

[Tech Blog] React Federation: How to create a micro React Application implement Module Federation…

A pragmatic guide to structuring complex frontend codebases

Composing Frontend Applications with Micro Frontends at Tray

Microfrontends in Practice: Part 1 of *