diff --git a/.changeset/breezy-lemons-grin.md b/.changeset/breezy-lemons-grin.md new file mode 100644 index 00000000000..389846d7a9d --- /dev/null +++ b/.changeset/breezy-lemons-grin.md @@ -0,0 +1,5 @@ +--- +'@module-federation/bridge-react': patch +--- + +feat(bridge-react): export createLazyCompoent api diff --git a/.changeset/curly-insects-wonder.md b/.changeset/curly-insects-wonder.md new file mode 100644 index 00000000000..67413e0d0af --- /dev/null +++ b/.changeset/curly-insects-wonder.md @@ -0,0 +1,5 @@ +--- +'@module-federation/modern-js': patch +--- + +refactor(modern-js-plugin): add subpath react to export createLazyCompoent and wrapNoSSR apis diff --git a/.changeset/curly-pillows-give.md b/.changeset/curly-pillows-give.md new file mode 100644 index 00000000000..e6ad4e09837 --- /dev/null +++ b/.changeset/curly-pillows-give.md @@ -0,0 +1,5 @@ +--- +'@module-federation/bridge-vue3': minor +--- + +refactor(vue3-bridge): rename createRemoteComponent as createRemoteAppComponent diff --git a/.changeset/hip-hornets-exercise.md b/.changeset/hip-hornets-exercise.md new file mode 100644 index 00000000000..e57b68dd904 --- /dev/null +++ b/.changeset/hip-hornets-exercise.md @@ -0,0 +1,5 @@ +--- +'@module-federation/bridge-react': minor +--- + +refactor(bridge-react): rename createRemoteComponent as createRemoteAppComponent diff --git a/.changeset/tender-rocks-leave.md b/.changeset/tender-rocks-leave.md new file mode 100644 index 00000000000..92cda74e3f0 --- /dev/null +++ b/.changeset/tender-rocks-leave.md @@ -0,0 +1,5 @@ +--- +'@module-federation/modern-js': minor +--- + +refactor(modern-js-plugin): deprecate createRemoteComponent and createRemoteSSRComponent diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 87ed38eed5c..14b67163349 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -56,7 +56,7 @@ jobs: run: nproc - name: Warm Nx Cache - run: npx nx run-many --targets=build --projects=tag:type:pkg --parallel=4 + run: npx nx run-many --targets=build --projects=tag:type:pkg --parallel=4 --skip-nx-cache - name: Run Build for All run: npx nx run-many --targets=build --projects=tag:type:pkg --parallel=4 --skip-nx-cache diff --git a/.github/workflows/devtools.yml b/.github/workflows/devtools.yml index fd263b433aa..bdcac4d5cf8 100644 --- a/.github/workflows/devtools.yml +++ b/.github/workflows/devtools.yml @@ -38,7 +38,7 @@ jobs: run: pnpm install - name: Run Affected Build - run: npx nx run-many --targets=build --projects=tag:type:pkg + run: npx nx run-many --targets=build --projects=tag:type:pkg --skip-nx-cache - name: Configuration xvfb shell: bash diff --git a/apps/modern-component-data-fetch/host/src/routes/basic/page.tsx b/apps/modern-component-data-fetch/host/src/routes/basic/page.tsx index 03de3de3524..ad3c3fd73fb 100644 --- a/apps/modern-component-data-fetch/host/src/routes/basic/page.tsx +++ b/apps/modern-component-data-fetch/host/src/routes/basic/page.tsx @@ -1,8 +1,7 @@ -import { kit, ERROR_TYPE } from '@module-federation/modern-js/runtime'; +import { getInstance } from '@module-federation/modern-js/runtime'; +import { ERROR_TYPE } from '@module-federation/modern-js/react'; -const { createRemoteComponent } = kit; - -const Basic = createRemoteComponent({ +const Basic = getInstance()!.createLazyComponent({ loader: () => { return import('remote/BasicComponent'); }, diff --git a/apps/modern-component-data-fetch/host/src/routes/client-downgrade/page.tsx b/apps/modern-component-data-fetch/host/src/routes/client-downgrade/page.tsx index b01971f8aed..32ae80ad2fe 100644 --- a/apps/modern-component-data-fetch/host/src/routes/client-downgrade/page.tsx +++ b/apps/modern-component-data-fetch/host/src/routes/client-downgrade/page.tsx @@ -1,8 +1,6 @@ -import { kit } from '@module-federation/modern-js/runtime'; +import { getInstance } from '@module-federation/modern-js/runtime'; -const { createRemoteComponent } = kit; - -const ClientDowngrade = createRemoteComponent({ +const ClientDowngrade = getInstance()!.createLazyComponent({ loader: () => { return import('remote/ClientDowngrade'); }, diff --git a/apps/modern-component-data-fetch/host/src/routes/csr/page.tsx b/apps/modern-component-data-fetch/host/src/routes/csr/page.tsx index 80fed2d9778..261093cd4d2 100644 --- a/apps/modern-component-data-fetch/host/src/routes/csr/page.tsx +++ b/apps/modern-component-data-fetch/host/src/routes/csr/page.tsx @@ -1,9 +1,9 @@ -import { kit } from '@module-federation/modern-js/runtime'; +import { getInstance } from '@module-federation/modern-js/runtime'; -const { createRemoteComponent, wrapNoSSR } = kit; - -const CsrWithFetchDataFromServerComponent = wrapNoSSR(createRemoteComponent)({ +const CsrWithFetchDataFromServerComponent = getInstance()!.createLazyComponent({ + noSSR: true, loader: () => { + console.log('calling'); return import('provider-csr'); }, loading: 'loading...', diff --git a/apps/modern-component-data-fetch/host/src/routes/layout.tsx b/apps/modern-component-data-fetch/host/src/routes/layout.tsx index 25ce86d7e1a..ec55820a6b4 100644 --- a/apps/modern-component-data-fetch/host/src/routes/layout.tsx +++ b/apps/modern-component-data-fetch/host/src/routes/layout.tsx @@ -1,6 +1,18 @@ import React from 'react'; import { Outlet, useNavigate, useLocation } from '@modern-js/runtime/router'; import { Layout, Menu } from 'antd'; +import { getInstance } from '@module-federation/modern-js/runtime'; +import { lazyLoadComponentPlugin } from '@module-federation/modern-js/react'; + +getInstance()!.registerPlugins([lazyLoadComponentPlugin()]); +console.log('layout'); +getInstance()!.prefetch({ + id: 'remote/BasicComponent', +}); + +getInstance()!.prefetch({ + id: 'provider-csr', +}); const { Header, Content } = Layout; diff --git a/apps/modern-component-data-fetch/host/src/routes/server-downgrade/page.tsx b/apps/modern-component-data-fetch/host/src/routes/server-downgrade/page.tsx index d4d2a5661c7..cae8591ba5a 100644 --- a/apps/modern-component-data-fetch/host/src/routes/server-downgrade/page.tsx +++ b/apps/modern-component-data-fetch/host/src/routes/server-downgrade/page.tsx @@ -1,8 +1,6 @@ -import { kit } from '@module-federation/modern-js/runtime'; +import { getInstance } from '@module-federation/modern-js/runtime'; -const { createRemoteComponent } = kit; - -const ServerDowngrade = createRemoteComponent({ +const ServerDowngrade = getInstance()!.createLazyComponent({ loader: () => { return import('remote/ServerDowngrade'); }, diff --git a/apps/modern-component-data-fetch/provider-csr/module-federation.config.ts b/apps/modern-component-data-fetch/provider-csr/module-federation.config.ts index bc0fc0ec85d..e8784e9e8bc 100644 --- a/apps/modern-component-data-fetch/provider-csr/module-federation.config.ts +++ b/apps/modern-component-data-fetch/provider-csr/module-federation.config.ts @@ -1,4 +1,4 @@ -import { createModuleFederationConfig } from '@module-federation/modern-js'; +import { createModuleFederationConfig } from '@module-federation/rsbuild-plugin'; export default createModuleFederationConfig({ name: 'provider_csr', diff --git a/apps/modern-component-data-fetch/provider-csr/package.json b/apps/modern-component-data-fetch/provider-csr/package.json index beee230d6b9..5ef916dfc48 100644 --- a/apps/modern-component-data-fetch/provider-csr/package.json +++ b/apps/modern-component-data-fetch/provider-csr/package.json @@ -8,6 +8,7 @@ "build:watch": "rslib build --watch" }, "devDependencies": { + "@module-federation/bridge-react": "workspace:*", "@module-federation/enhanced": "workspace:*", "@module-federation/rsbuild-plugin": "workspace:*", "@module-federation/storybook-addon": "workspace:*", diff --git a/apps/modern-component-data-fetch/provider-csr/src/index.data.ts b/apps/modern-component-data-fetch/provider-csr/src/index.data.ts index 9cbe4997e71..4a1391087d5 100644 --- a/apps/modern-component-data-fetch/provider-csr/src/index.data.ts +++ b/apps/modern-component-data-fetch/provider-csr/src/index.data.ts @@ -1,8 +1,10 @@ +import { cache } from '@module-federation/bridge-react'; export type Data = { data: string; }; -export const fetchData = async (): Promise => { +export const fetchData = cache(async (): Promise => { + console.log(`[ csr provider - server ] fetch data: ${new Date()}`); return new Promise((resolve) => { setTimeout(() => { resolve({ @@ -10,4 +12,4 @@ export const fetchData = async (): Promise => { }); }, 1000); }); -}; +}); diff --git a/apps/modern-component-data-fetch/provider/src/components/BasicComponent/index.data.ts b/apps/modern-component-data-fetch/provider/src/components/BasicComponent/index.data.ts index 5586247ab1c..bf4230c2149 100644 --- a/apps/modern-component-data-fetch/provider/src/components/BasicComponent/index.data.ts +++ b/apps/modern-component-data-fetch/provider/src/components/BasicComponent/index.data.ts @@ -1,8 +1,11 @@ +import { cache } from '@module-federation/modern-js/react'; + export type Data = { data: string; }; -export const fetchData = async (): Promise => { +export const fetchData = cache(async (): Promise => { + console.log('provder-server called'); return new Promise((resolve) => { setTimeout(() => { resolve({ @@ -10,4 +13,4 @@ export const fetchData = async (): Promise => { }); }, 1000); }); -}; +}); diff --git a/apps/modernjs-ssr/dynamic-nested-remote/src/components/Content.tsx b/apps/modernjs-ssr/dynamic-nested-remote/src/components/Content.tsx index 312936b2d32..bdbfe4644c2 100644 --- a/apps/modernjs-ssr/dynamic-nested-remote/src/components/Content.tsx +++ b/apps/modernjs-ssr/dynamic-nested-remote/src/components/Content.tsx @@ -1,13 +1,14 @@ import React from 'react'; import Button from 'antd/lib/button'; import { + getInstance, registerRemotes, loadRemote, - kit, } from '@module-federation/modern-js/runtime'; +import { lazyLoadComponentPlugin } from '@module-federation/modern-js/react'; import stuff from './stuff.module.css'; -const { createRemoteSSRComponent } = kit; +getInstance()!.registerPlugins([lazyLoadComponentPlugin()]); registerRemotes([ { @@ -16,7 +17,7 @@ registerRemotes([ }, ]); -const RemoteSSRComponent = createRemoteSSRComponent({ +const RemoteSSRComponent = getInstance()!.createLazyComponent({ loader: () => loadRemote('dynamic_remote'), loading: 'loading...', fallback: ({ error }) => { diff --git a/apps/modernjs-ssr/host/src/routes/dynamic-remote/page.tsx b/apps/modernjs-ssr/host/src/routes/dynamic-remote/page.tsx index d95ab03769b..6f54211172a 100644 --- a/apps/modernjs-ssr/host/src/routes/dynamic-remote/page.tsx +++ b/apps/modernjs-ssr/host/src/routes/dynamic-remote/page.tsx @@ -1,12 +1,10 @@ import React, { useState, Suspense } from 'react'; import { + getInstance, loadRemote, registerRemotes, - kit, } from '@module-federation/modern-js/runtime'; -const { createRemoteSSRComponent } = kit; - registerRemotes([ { name: 'dynamic_remote', @@ -14,7 +12,7 @@ registerRemotes([ }, ]); -const RemoteSSRComponent = createRemoteSSRComponent({ +const RemoteSSRComponent = getInstance()!.createLazyComponent({ loader: () => loadRemote('dynamic_remote'), loading: 'loading...', fallback: ({ error }) => { diff --git a/apps/modernjs-ssr/host/src/routes/layout.tsx b/apps/modernjs-ssr/host/src/routes/layout.tsx index 6cd5fc73bcb..ae85bd69429 100644 --- a/apps/modernjs-ssr/host/src/routes/layout.tsx +++ b/apps/modernjs-ssr/host/src/routes/layout.tsx @@ -1,9 +1,11 @@ import React from 'react'; import { Outlet, useNavigate } from '@modern-js/runtime/router'; import { Layout, Menu } from 'antd'; - +import { lazyLoadComponentPlugin } from '@module-federation/modern-js/react'; +import { getInstance } from '@module-federation/modern-js/runtime'; const { Header, Content } = Layout; +getInstance()!.registerPlugins([lazyLoadComponentPlugin()]); const App: React.FC = () => { const navi = useNavigate(); diff --git a/apps/modernjs-ssr/nested-remote/src/routes/page.tsx b/apps/modernjs-ssr/nested-remote/src/routes/page.tsx index 84b8509ef14..1dfb9347068 100644 --- a/apps/modernjs-ssr/nested-remote/src/routes/page.tsx +++ b/apps/modernjs-ssr/nested-remote/src/routes/page.tsx @@ -1,11 +1,12 @@ -import { kit } from '@module-federation/modern-js/runtime'; +import { getInstance } from '@module-federation/modern-js/runtime'; +import { lazyLoadComponentPlugin } from '@module-federation/modern-js/react'; import Content from '../components/Content'; import './index.css'; -const { createRemoteSSRComponent } = kit; +getInstance()!.registerPlugins([lazyLoadComponentPlugin()]); -const RemoteSSRComponent = createRemoteSSRComponent({ +const RemoteSSRComponent = getInstance()!.createLazyComponent({ loader: () => import('remote/Button'), loading: 'loading...', export: 'Button', diff --git a/apps/router-demo/router-host-2000/src/App.tsx b/apps/router-demo/router-host-2000/src/App.tsx index 5412248dd30..562ab57da1a 100644 --- a/apps/router-demo/router-host-2000/src/App.tsx +++ b/apps/router-demo/router-host-2000/src/App.tsx @@ -7,7 +7,7 @@ import React, { import { Route, Routes, useLocation } from 'react-router-dom'; import { init, loadRemote } from '@module-federation/enhanced/runtime'; import { RetryPlugin } from '@module-federation/retry-plugin'; -import { createRemoteComponent } from '@module-federation/bridge-react'; +import { createRemoteAppComponent } from '@module-federation/bridge-react'; import Navigation from './navigation'; import Detail from './pages/Detail'; import Home from './pages/Home'; @@ -67,13 +67,13 @@ const FallbackErrorComp = (info: any) => { const FallbackComp =
loading...
; -const Remote1App = createRemoteComponent({ +const Remote1App = createRemoteAppComponent({ loader: () => loadRemote('remote1/export-app'), fallback: FallbackErrorComp, loading: FallbackComp, }); -const Remote5App = createRemoteComponent({ +const Remote5App = createRemoteAppComponent({ loader: () => loadRemote('remote5/export-app'), fallback: FallbackErrorComp, loading: FallbackComp, @@ -136,26 +136,26 @@ const Remote1AppWithErrorBoundary = React.forwardRef((props, ref) => ( )); -const Remote2App = createRemoteComponent({ +const Remote2App = createRemoteAppComponent({ loader: () => import('remote2/export-app'), export: 'provider', fallback: FallbackErrorComp, loading: FallbackComp, }); -const Remote3App = createRemoteComponent({ +const Remote3App = createRemoteAppComponent({ loader: () => loadRemote('remote3/export-app'), fallback: FallbackErrorComp, loading: FallbackComp, }); -const RemoteRenderErrorApp = createRemoteComponent({ +const RemoteRenderErrorApp = createRemoteAppComponent({ loader: () => loadRemote('remote-render-error/export-app'), fallback: FallbackErrorComp, loading: FallbackComp, }) as ForwardRefExoticComponent; -const RemoteResourceErrorApp = createRemoteComponent({ +const RemoteResourceErrorApp = createRemoteAppComponent({ loader: () => loadRemote('remote-resource-error/export-app'), fallback: FallbackErrorComp, loading: FallbackComp, diff --git a/apps/router-demo/router-host-2000/src/pages/Home.tsx b/apps/router-demo/router-host-2000/src/pages/Home.tsx index e31d64c2bf5..c4a281906fc 100644 --- a/apps/router-demo/router-host-2000/src/pages/Home.tsx +++ b/apps/router-demo/router-host-2000/src/pages/Home.tsx @@ -3,7 +3,7 @@ import { Space, Table, Tag } from 'antd'; import type { TableProps } from 'antd'; import type React from 'react'; import { init, loadRemote } from '@module-federation/enhanced/runtime'; -import { createRemoteComponent } from '@module-federation/bridge-react'; +import { createRemoteAppComponent } from '@module-federation/bridge-react'; interface DataType { key: string; @@ -86,7 +86,7 @@ const data: DataType[] = [ }, ]; -const Remote1Button = createRemoteComponent({ +const Remote1Button = createRemoteAppComponent({ loader: () => loadRemote('remote1/export-button'), // @ts-ignore fallback: null, diff --git a/apps/router-demo/router-host-v5-2200/src/App.tsx b/apps/router-demo/router-host-v5-2200/src/App.tsx index 9b7200465a3..bdac28f68a1 100644 --- a/apps/router-demo/router-host-v5-2200/src/App.tsx +++ b/apps/router-demo/router-host-v5-2200/src/App.tsx @@ -4,7 +4,7 @@ import './App.css'; import Navigation from './navigation'; import Home from './pages/Home'; import { loadRemote } from '@module-federation/enhanced/runtime'; -import { createRemoteComponent } from '@module-federation/bridge-react'; +import { createRemoteAppComponent } from '@module-federation/bridge-react'; const FallbackErrorComp = (info: any) => { return
{info?.error?.message}
; @@ -12,13 +12,13 @@ const FallbackErrorComp = (info: any) => { const FallbackComp =
loading
; -const Remote1App = createRemoteComponent({ +const Remote1App = createRemoteAppComponent({ loader: () => loadRemote('remote1/export-app'), fallback: FallbackErrorComp, loading: FallbackComp, }); -const Remote2App = createRemoteComponent({ +const Remote2App = createRemoteAppComponent({ // @ts-ignore loader: () => import('remote2/export-app'), export: 'provider', @@ -26,7 +26,7 @@ const Remote2App = createRemoteComponent({ loading: FallbackComp, }); -const Remote3App = createRemoteComponent({ +const Remote3App = createRemoteAppComponent({ loader: () => loadRemote('remote3/export-app'), fallback: FallbackErrorComp, loading: FallbackComp, diff --git a/apps/router-demo/router-host-vue3-2100/src/router.ts b/apps/router-demo/router-host-vue3-2100/src/router.ts index 450339df79f..a1a30cc29b9 100644 --- a/apps/router-demo/router-host-vue3-2100/src/router.ts +++ b/apps/router-demo/router-host-vue3-2100/src/router.ts @@ -8,7 +8,7 @@ import Detail from '@/pages/Detail.vue'; import { loadRemote, init } from '@module-federation/enhanced/runtime'; import * as bridge from '@module-federation/bridge-vue3'; -const Remote2 = bridge.createRemoteComponent({ +const Remote2 = bridge.createRemoteAppComponent({ loader: () => loadRemote('remote1/export-app'), }); diff --git a/apps/website-new/docs/en/_meta.json b/apps/website-new/docs/en/_nav.json similarity index 100% rename from apps/website-new/docs/en/_meta.json rename to apps/website-new/docs/en/_nav.json diff --git a/apps/website-new/docs/en/guide/basic/_meta.json b/apps/website-new/docs/en/guide/basic/_meta.json index 78addf012d9..20b6f784569 100644 --- a/apps/website-new/docs/en/guide/basic/_meta.json +++ b/apps/website-new/docs/en/guide/basic/_meta.json @@ -4,5 +4,10 @@ "name": "runtime", "label": "Runtime" }, -"rsbuild", "rspack", "webpack", "rspress","vite","type-prompt","cli", "css-isolate" +"rsbuild", "rspack", "webpack", "rspress","vite","type-prompt","cli", "css-isolate", + { + "type": "dir", + "name": "data-fetch", + "label": "Data Solution" + } ] diff --git a/apps/website-new/docs/en/guide/basic/data-fetch/_meta.json b/apps/website-new/docs/en/guide/basic/data-fetch/_meta.json new file mode 100644 index 00000000000..4428d129a59 --- /dev/null +++ b/apps/website-new/docs/en/guide/basic/data-fetch/_meta.json @@ -0,0 +1 @@ +["index", "cache", "prefetch"] diff --git a/apps/website-new/docs/en/guide/basic/data-fetch/cache.mdx b/apps/website-new/docs/en/guide/basic/data-fetch/cache.mdx new file mode 100644 index 00000000000..80df73bf19b --- /dev/null +++ b/apps/website-new/docs/en/guide/basic/data-fetch/cache.mdx @@ -0,0 +1,326 @@ +# Data Caching + +The `cache` function allows you to cache the results of data fetching or computations. It provides fine-grained control over data and is suitable for various scenarios, including Client-Side Rendering (CSR), Server-Side Rendering (SSR), and API services (BFF). + +## Basic Usage + +```ts +import { cache } from '@module-federation/bridge-react'; + +export type Data = { + data: string; +}; + +export const fetchData = cache(async (): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + data: `[ provider ] fetched data: ${new Date()}`, + }); + }, 1000); + }); +}); +``` + +### Parameters + +- `fetchData`: The `fetchData` function in a DataLoader. +- `options` (optional): Cache configuration. + - `tag`: A tag to identify the cache, which can be used to invalidate it. + - `maxAge`: The cache's validity period (in milliseconds). + - `revalidate`: A time window for revalidating the cache (in milliseconds), similar to the `stale-while-revalidate` feature of HTTP Cache-Control. + - `getKey`: A simplified cache key generation function that creates a key based on the function's arguments. + - `onCache`: A callback function for when data is cached, used for custom handling of cached data. + +The type for the `options` parameter is as follows: + +```ts +interface CacheOptions { + tag?: string | string[]; + maxAge?: number; + revalidate?: number; + getKey?: (...args: Args) => string; + onCache?: (info: CacheStatsInfo) => boolean; +} +``` + +### Return Value + +The `cache` function returns a new `fetchData` function with caching capabilities. Calling this new function multiple times will not result in repeated execution. + +## Scope of Use + +This function is only supported for use within a DataLoader. + +## Detailed Usage + +### `maxAge` + +After each computation, the framework records the time the data was written to the cache. When the function is called again, it checks if the cache has expired based on the `maxAge` parameter. If it has, the `fn` function is re-executed; otherwise, the cached data is returned. + +```ts +import { cache, CacheTime } from '@module-federation/bridge-react'; + +const getDashboardStats = cache( + async () => { + return await fetchComplexStatistics(); + }, + { + maxAge: CacheTime.MINUTE * 2, // Calling this function within 2 minutes will return cached data. + } +); +``` + +#### `revalidate` + +The `revalidate` parameter sets a time window for revalidating the cache after it has expired. It can be used with the `maxAge` parameter, similar to the `stale-while-revalidate` model of HTTP Cache-Control. + +In the following example, if `getDashboardStats` is called within the 2-minute non-expired window, it returns cached data. If the cache is expired (between 2 and 3 minutes), incoming requests will first receive the old data, and then a new request will be made in the background to update the cache. + +```ts +import { cache, CacheTime } from '@module-federation/bridge-react'; + +const getDashboardStats = cache( + async () => { + return await fetchComplexStatistics(); + }, + { + maxAge: CacheTime.MINUTE * 2, + revalidate: CacheTime.MINUTE * 1, + } +); +``` + +#### `tag` + +The `tag` parameter is used to identify a cache with a label, which can be a string or an array of strings. This tag can be used to invalidate the cache, and multiple cache functions can share the same tag. + +```ts +import { cache, revalidateTag } from '@module-federation/bridge-react'; + +const getDashboardStats = cache( + async () => { + return await fetchDashboardStats(); + }, + { + tag: 'dashboard', + } +); + +const getComplexStatistics = cache( + async () => { + return await fetchComplexStatistics(); + }, + { + tag: 'dashboard', + } +); + +revalidateTag('dashboard-stats'); // This will invalidate the caches for both getDashboardStats and getComplexStatistics. +``` + +#### `getKey` + +The `getKey` parameter allows you to customize how cache keys are generated. For example, you might only need to rely on a subset of the function's parameters to differentiate caches. It is a function that receives the same arguments as the original function and returns a string as the cache key: + +```ts +import { cache, CacheTime } from '@module-federation/bridge-react'; +import { fetchUserData } from './api'; + +const getUser = cache( + async (userId, options) => { + // Here, options might contain many configurations, but we only want to cache based on userId. + return await fetchUserData(userId, options); + }, + { + maxAge: CacheTime.MINUTE * 5, + // Only use the first parameter (userId) as the cache key. + getKey: (userId, options) => userId, + } +); + +// The following two calls will share the cache because getKey only uses userId. +await getUser(123, { language: 'zh' }); +await getUser(123, { language: 'en' }); // Hits the cache, no new request. + +// Different userIds will use different caches. +await getUser(456, { language: 'zh' }); // Does not hit the cache, new request. +``` + +You can also use the `generateKey` function provided by Modern.js with `getKey` to generate cache keys: + +:::info + +The `generateKey` function in Modern.js ensures that a consistent, unique key is generated even if the order of object properties changes, guaranteeing stable caching. + +::: + +```ts +import { cache, CacheTime, generateKey } from '@module-federation/bridge-react'; +import { fetchUserData } from './api'; + +const getUser = cache( + async (userId, options) => { + return await fetchUserData(userId, options); + }, + { + maxAge: CacheTime.MINUTE * 5, + getKey: (userId, options) => generateKey(userId), + } +); +``` + +#### `customKey` + +The `customKey` parameter is used to customize the cache key. It is a function that receives an object with the following properties and must return a string or Symbol to be used as the cache key: + +- `params`: An array of arguments passed to the cached function. +- `fn`: A reference to the original cached function. +- `generatedKey`: The original cache key automatically generated by the framework based on the input parameters. + +:::info + +Generally, the cache will be invalidated in the following scenarios: +1. The reference to the cached function changes. +2. The function's input parameters change. +3. The `maxAge` condition is not met. +4. `revalidateTag` is called. + +`customKey` can be used in scenarios where you want to share a cache even if the function reference is different. If you only need to customize the cache key, it is recommended to use `getKey`. + +::: + +This is very useful in certain scenarios, such as when the function reference changes, but you still want to return cached data. + +```ts +import { cache } from '@module-federation/bridge-react'; +import { fetchUserData } from './api'; + +// Different function references, but they can share a cache via customKey. +const getUserA = cache( + fetchUserData, + { + maxAge: CacheTime.MINUTE * 5, + customKey: ({ params }) => { + // Return a stable string as the cache key. + return `user-${params[0]}`; + }, + } +); + +// Even if the function reference changes, it will hit the cache as long as customKey returns the same value. +const getUserB = cache( + (...args) => fetchUserData(...args), // New function reference. + { + maxAge: CacheTime.MINUTE * 5, + customKey: ({ params }) => { + // Return the same key as getUserA. + return `user-${params[0]}`; + }, + } +); + +// Even though getUserA and getUserB are different function references, because their customKey returns the same value, +// they will share the cache when called with the same parameters. +const dataA = await getUserA(1); +const dataB = await getUserB(1); // This will hit the cache and not make a new request. + +// You can also use a Symbol as the cache key (usually for sharing a cache within the same application). +const USER_CACHE_KEY = Symbol('user-cache'); +const getUserC = cache( + fetchUserData, + { + maxAge: CacheTime.MINUTE * 5, + customKey: () => USER_CACHE_KEY, + } +); + +// You can also use the generatedKey parameter to modify the default key. +const getUserD = cache( + fetchUserData, + { + customKey: ({ generatedKey }) => `prefix-${generatedKey}`, + } +); +``` + +#### `onCache` + +The `onCache` parameter allows you to track cache statistics, such as hit rates. It is a callback function that receives information about each cache operation, including its status, key, parameters, and result. You can return `false` from `onCache` to prevent a cache hit. + +```ts +import { cache, CacheTime } from '@module-federation/bridge-react'; + +// Track cache statistics. +const stats = { + total: 0, + hits: 0, + misses: 0, + stales: 0, + hitRate: () => stats.hits / stats.total +}; + +const getUser = cache( + fetchUserData, + { + maxAge: CacheTime.MINUTE * 5, + onCache({ status, key, params, result }) { + // status can be 'hit', 'miss', or 'stale'. + stats.total++; + + if (status === 'hit') { + stats.hits++; + } else if (status === 'miss') { + stats.misses++; + } else if (status === 'stale') { + stats.stales++; + } + + console.log(`Cache ${status === 'hit' ? 'hit' : status === 'miss' ? 'miss' : 'stale'}, key: ${String(key)}`); + console.log(`Current hit rate: ${stats.hitRate() * 100}%`); + } + } +); + +// Example usage: +await getUser(1); // Cache miss. +await getUser(1); // Cache hit. +await getUser(2); // Cache miss. +``` + +The `onCache` callback receives an object with the following properties: + +- `status`: The status of the cache operation, which can be: + - `hit`: Cache hit, returns cached content. + - `miss`: Cache miss, executes the function and caches the result. + - `stale`: Cache hit but the data is stale, returns cached content and revalidates in the background. +- `key`: The cache key, which may be the result of `customKey` or the default generated key. +- `params`: The arguments passed to the cached function. +- `result`: The result data (either from the cache or newly computed). + +This callback is only called when the `options` parameter is provided. When using a cache function without options, the `onCache` callback is not invoked. + +The `onCache` callback is very useful for: +- Monitoring cache performance. +- Calculating hit rates. +- Logging cache operations. +- Implementing custom metrics. + +### Storage + +Currently, whether on the client or server, the cache is stored in memory. By default, the shared storage limit for all cache functions is 1GB. When this limit is reached, the LRU (Least Recently Used) algorithm is used to remove old caches. + +:::info +Considering that the content cached by the `cache` function is not expected to be very large, it is currently stored in memory by default. +::: + +You can specify the cache storage limit using the `configureCache` function: + +```ts +import { configureCache, CacheSize } from '@module-federation/bridge-react'; + +configureCache({ + maxSize: CacheSize.MB * 10, // 10MB +}); +``` + diff --git a/apps/website-new/docs/en/guide/basic/data-fetch/index.mdx b/apps/website-new/docs/en/guide/basic/data-fetch/index.mdx new file mode 100644 index 00000000000..33aba2c989a --- /dev/null +++ b/apps/website-new/docs/en/guide/basic/data-fetch/index.mdx @@ -0,0 +1,201 @@ +# Data Fetching + +:::tip +Data Loader supports both SSR and CSR scenarios! + +* [Demo](https://github.com/module-federation/core/tree/main/apps/modern-component-data-fetch) + +::: + +## Introduction + +In SSR scenarios, `useEffect` does not execute. This behavior means that under normal circumstances, it's impossible to fetch data before rendering a component. + +To support this functionality, mainstream frameworks typically pre-fetch data using the `data loader` provided by React Router and inject it into the route component. The route component then retrieves the data using [useLoaderData](https://reactrouter.com/api/hooks/useLoaderData#useloaderdata) for rendering. + +This approach heavily relies on routing functionality and cannot be used directly with {props.name || 'Module Federation'}. + +To solve this problem, {props.name || 'Module Federation'} provides **component-level** data fetching capabilities, allowing developers to fetch data and render components in SSR scenarios. + +:::tip What is component-level? +The use of {props.name || 'Module Federation'} can be broadly divided into two parts: components (functions) and applications. The difference between them is whether they include *routing* functionality. +::: + +## How to Use + +Different actions are required depending on the role. + +### Producer + +import React from 'react'; +import ProviderTip from '@components/en/data-fetch/provider-tip'; + +{props.providerTip || React.createElement(ProviderTip)} + +Each exposed module can have a corresponding `.data` file with the same name. These files can export a loader function, which we call a Data Loader. It executes before the corresponding `expose` component renders, providing data to the component. Here is an example: + +```bash +. +└── src + ├── List.tsx + └── List.data.ts +``` + +Here, `List.data.ts` needs to export a function named `fetchData`, which will be executed before the `List` component renders and injects its data. Here is an example: + +```ts title="List.data.ts" +import type { DataFetchParams } from '@module-federation/bridge-react'; +export type Data = { + data: string; +}; + +export const fetchData = async (params: DataFetchParams): Promise => { + console.log('params: ', params); + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + data: `data: ${new Date()}`, + }); + }, 1000); + }); +}; +``` + +The data from the loader function is injected into the producer's props with the key `mfData`. Therefore, the producer needs to modify the code to consume this data, as shown below: + +```tsx title="List.tsx" +import React from 'react'; +import type { Data } from './index.data'; + +const List = (props: { + mfData?: Data; +}): JSX.Element => { + return ( +
+ {props.mfData?.data && props.mfData?.data.map((item,index)=>

{item}

)} +
+ ); +}; + +export default List; +``` + +##### Producer Consuming Its Own Data + +If you use Modern.js to develop a producer, and that producer's page is also accessed directly, you can use the [Data Loader](https://modernjs.dev/guides/basic-features/data/data-fetch.html) provided by Modern.js to inject data. + +Its usage is almost identical to {props.name || 'Module Federation'}'s, except for the function name. This makes it easy to consume the Data Loader in the producer. Here's an example: + +* Create a `page.data.ts` file in the producer's page directory and export a function named `loader`: + +```ts title="page.data.ts" +import { fetchData } from '../components/List.data'; +import type { Data } from '../components/List.data'; + +export const loader = fetchData + +export type {Data} +``` + +* Consume this data on the producer's `page`: + +```tsx title="page.tsx" +import { useLoaderData } from '@modern-js/runtime/router'; +import List from '../components/List'; +import './index.css'; + +import type { Data } from './page.data'; + +const Index = () => { + const data = useLoaderData() as Data; + console.log('page data', data); + + return ( +
+ +
+)}; + +export default Index; + +``` + +### Consumer + +import Consumer from '@components/en/data-fetch/consumer'; + +{props.consumer || React.createElement(Consumer)} + +### Loader Function + +#### Parameters + +By default, parameters are passed to the loader function. The type is [dataFetchParams](../../../practice/bridge/react-bridge#datafetchparams), which includes the following field: + +- `isDowngrade` (boolean): Indicates whether the current execution context is in a fallback mode. For example, if Server-Side Rendering (SSR) fails, a new request is sent from the client-side (CSR) to the server to call the loader function. In this case, the value is `true`. + +In addition to the default parameters, you can also pass the `dataFetchParams` field in `createLazyComponent`, which will be passed through to the loader function. + +#### Return Value + +The return value of the loader function can only be a **serializable data object**. + +### Using Data Loader in Different Environments + +The loader function can be executed on the server or in the browser. A loader function executed on the server is called a Server Loader, and one executed in the browser is called a Client Loader. + +In CSR applications, the loader function is executed in the browser, so they are all Client Loaders by default. + +In SSR applications, the loader function is only executed on the server, so they are all Server Loaders by default. In SSR, {props.name || 'Module Federation'} directly calls the corresponding loader function on the server. When switching routes in the browser, {props.name || 'Module Federation'} sends an HTTP request to the SSR service, which also triggers the loader function on the server. + +:::note NOTE + +Executing the loader function only on the server in SSR applications offers the following benefits: + +- **Simplified Usage**: It ensures that the data fetching method in SSR applications is isomorphic, so developers don't need to differentiate code execution based on the environment. +- **Reduced Browser Bundle Size**: Logic code and its dependencies are moved from the browser to the server. +- **Improved Maintainability**: Moving logic code to the server reduces the direct impact of data logic on the front-end UI. It also prevents accidentally including server-side dependencies in the browser bundle or vice versa. + +::: + +#### Using Client Loader in SSR Applications + +By default, in SSR applications, the loader function is only executed on the server. However, in some scenarios, developers may want requests from the browser to go directly to the data source without passing through the SSR service. For example: + +1. To reduce network consumption in the browser by directly requesting the data source. +2. The application has a data cache in the browser and does not want to request data from the SSR service. + +{props.name || 'Module Federation'} supports adding an additional `.data.client` file in SSR applications, which also exports a named loader. In this case, if the Data Loader on the server fails and falls back, or when switching routes in the browser, the application will execute this loader function in the browser like a CSR application, instead of sending another data request to the SSR service. + +```ts title="List.data.client.ts" +import cache from 'my-cache'; + +export async function loader({ params }) { + if (cache.has(params.id)) { + return cache.get(params.id); + } + const res = await fetch('URL_ADDRESS?id={params.id}'); + return { + message: res.message, + } +} +``` +:::warning WARNING + +To use a Client Loader, there must be a corresponding Server Loader, and the Server Loader must be defined in a `.data` file, not a `.loader` file. + +::: + +## FAQ + +### Application-Level Data Fetching? + +For application-level modules, we prefer to use RSC (React Server Components) to make the functionality more complete. This feature is currently under exploration, so please stay tuned. + +### Is Nested Producer Supported? + +No, it is not supported. + +### Are there other plugins for producers besides the Rslib plugin and the Modern.js plugin? + +Currently, only the Rslib and Modern.js plugins can create a Data Loader. diff --git a/apps/website-new/docs/en/guide/basic/data-fetch/prefetch.mdx b/apps/website-new/docs/en/guide/basic/data-fetch/prefetch.mdx new file mode 100644 index 00000000000..76ae33689a9 --- /dev/null +++ b/apps/website-new/docs/en/guide/basic/data-fetch/prefetch.mdx @@ -0,0 +1,46 @@ +# Prefetch + +The `prefetch` function is used to pre-fetch resources and **data** for remote modules, thereby improving application performance and user experience. By pre-loading required content before a user accesses a feature, waiting times can be significantly reduced. + +import React from 'react'; +import PrefetchTip from '@components/en/data-fetch/prefetch-tip'; + +{props.prefetchTip || React.createElement(PrefetchTip)} + +## When to Use + +`prefetch` is an ideal choice when you want to pre-load associated JavaScript, CSS, or data for a component without immediately rendering it. For example, you can trigger `prefetch` when a user hovers over a link or button, so that when the user actually clicks, the component can be rendered faster. + +## API + +```typescript +interface FederationHost { + prefetch(options: { + id: string; + dataFetchParams?: Record; + preloadComponentResource?: boolean; + }): void; +} +``` + +### Parameters + +- `id` (required): `string` + The unique identifier for the remote module, in the format `'/'`. For example, `'shop/Button'`. + +- `preloadComponentResource` (optional): `boolean` + Whether to preload the component's resources, including JavaScript chunks and associated CSS files. Defaults to `false`. + +- `dataFetchParams` (optional): `object` + If the remote component has a data fetch function, this object will be passed to it. + + +## Usage Examples + +Suppose we have a remote application `shop` that exposes a `Button` component, and this component is associated with a data fetch function. + +import PrefetchDemo from '@components/en/data-fetch/prefetch-demo' + +{props.prefetchDemo || React.createElement(PrefetchDemo)} + +By using `prefetch` flexibly, you can finely control the timing of resource loading based on your application's specific scenarios and user behavior, thereby optimizing application performance. diff --git a/apps/website-new/docs/en/guide/framework/modernjs.mdx b/apps/website-new/docs/en/guide/framework/modernjs.mdx index 5286aeb6ad3..d1f293ca841 100644 --- a/apps/website-new/docs/en/guide/framework/modernjs.mdx +++ b/apps/website-new/docs/en/guide/framework/modernjs.mdx @@ -77,402 +77,13 @@ There is no difference in using Module Federation in SSR scenarios compared to C ## Component-Level Data Fetch -### Introduction +See [Data Fetching](../basic/data-fetch/index). -In SSR scenarios, `useEffect` does not execute, which normally prevents fetching data before rendering a component. - -To support this functionality, mainstream frameworks typically prefetch data based on React Router's `data loader` and inject it into route components. Route components then access and render this data using [useLoaderData](https://reactrouter.com/api/hooks/useLoaderData#useloaderdata). - -This approach heavily relies on routing functionality and cannot be used directly with Module Federation. - -To address this issue, Module Federation provides **component-level** data fetching capabilities, enabling developers to fetch and render data in SSR scenarios. - -:::tip What does component-level mean? -Module Federation usage can be broadly categorized into two parts: components (functions) and applications. The difference lies in whether they include *routing* functionality. -::: - -### How to Use - -Different actions are required depending on the role. - -#### Provider -::: info Note -Producers can use [Rslib](../basic/rsbuild#ssr) to generate SSR components. - -However, it should be noted that because the data in "Data Fetching" is injected by the consumer. Therefore, if "Data Fetching" is used in Rslib, the exported non-MF components cannot be isomorphic with MF components. -::: - -Each exposed module can have a corresponding `.data` file with the same name. These files can export a loader function, which we call a Data Loader. This function executes before the corresponding `expose` component renders, providing data to the component. Here's an example: - -```bash -. -└── src - ├── List.tsx - └── List.data.ts -``` - -The `List.data.ts` file needs to export a function named `fetchData`. This function will execute before the `List` component renders and inject its data. Here's an example: - -```ts title="List.data.ts" -import type { DataFetchParams } from '@module-federation/modern-js/runtime'; -export type Data = { - data: string; -}; - -export const fetchData = async (params: DataFetchParams): Promise => { - console.log('params: ', params); - return new Promise((resolve) => { - setTimeout(() => { - resolve({ - data: `data: ${new Date()}`, - }); - }, 1000); - }); -}; -``` - -The data from the loader function is injected into the producer's props with the key `mfData`. Therefore, the producer needs to modify its code to consume this data, as shown in the example below: - -```tsx title="List.tsx" -import React from 'react'; -import type { Data } from './index.data'; - -const List = (props: { - mfData?: Data; -}): JSX.Element => { - return ( -
- {props.mfData?.data && props.mfData?.data.map((item,index)=>

{item}

)} -
- ); -}; - -export default List; -``` - - -##### Producer Consuming Its Own Data - -If Modern.js is used to develop a producer, and this producer page is also accessed independently, then Modern.js's [Data Loader](https://modernjs.dev/zh/guides/basic-features/data/data-fetch.html) can be used to inject data. - -Its usage is basically the same as Module Federation, except for the function name. Therefore, you can easily consume Data Loader in the producer, as shown in the example below: - -* Create a `page.data.ts` file in the producer page and export a function named `loader`: - -```ts title="page.data.ts" -import { fetchData } from '../components/List.data'; -import type { Data } from '../components/List.data'; - -export const loader = fetchData - -export type {Data} -``` -* Consume this data in the producer page : - -```tsx title="page.tsx" -import { useLoaderData } from '@modern-js/runtime/router'; -import List from '../components/List'; -import './index.css'; - -import type { Data } from './page.data'; - -const Index = () => { - const data = useLoaderData() as Data; - console.log('page data', data); - - return ( -
- -
-)}; - -export default Index; -``` - -#### Consumer - -In the consumer, we need to use `createRemoteComponent` to load the remote component and fetch data. - -```tsx -import { kit, ERROR_TYPE } from '@module-federation/modern-js/runtime'; - -const { createRemoteComponent } = kit; - -const List = createRemoteComponent({ - loader: () => { - return import('remote/List'); - }, - loading: 'loading...', - export: 'default', - fallback: ({error,errorType,dataFetchMapKey}) => { - console.error(error) - if(errorType === ERROR_TYPE.LOAD_REMOTE){ - return
load remote failed
- } - if(errorType === ERROR_TYPE.DATA_FETCH){ - return
data fetch failed, the dataFetchMapKey key is: {dataFetchMapKey}
- } - return
error type is unknown
; - }, -}); - -const Index = (): JSX.Element => { - return ( -
-

Basic usage with data fetch

- -
- ); -}; - -export default Index; -``` - -### Loader Function - -#### Parameters - -By default, parameters are passed to the loader function. The type is `DataFetchParams`, which includes the following field: - -- `isDowngrade` (boolean): Indicates whether the current execution context is in downgrade mode. For example, if Server-Side Rendering (SSR) fails and falls back to Client-Side Rendering (CSR), a request will be re-initiated to the server to call the loader function, and this value will be `true`. - -In addition to the default parameters, you can also pass the [dataFetchParams](#datafetchparams) field in `createRemoteComponent`. This field will be passed through to the loader function. - -#### Return Value - -The return value of the loader function can only be a **serializable data object**. - -### Using Data Loader in Different Environments - -The loader function may execute on the server or in the browser. A loader function executed on the server is called a Server Loader, and one executed in the browser is called a Client Loader. - -In CSR applications, the loader function executes in the browser, meaning they are all Client Loaders by default. - -In SSR applications, the loader function only executes on the server, meaning they are all Server Loaders by default. In SSR, Module Federation directly calls the corresponding loader function on the server. When switching routes in the browser, Module Federation sends an HTTP request to the SSR service, which also triggers the loader function on the server. - -:::note NOTE - -Having the loader function for SSR applications execute only on the server offers the following benefits: - -- Simplified usage: Ensures that data fetching in SSR applications is isomorphic, so developers don't need to differentiate loader function execution code based on the environment. -- Reduced browser bundle size: Moves logic code and its dependencies from the browser to the server. -- Improved maintainability: Moves logic code to the server, reducing the direct impact of data logic on the frontend UI. Additionally, it prevents accidental inclusion of server-side dependencies in the browser bundle or browser-side dependencies in the server bundle. - -::: - -#### Using Client Loader in SSR Applications - -By default, in SSR applications, the loader function only executes on the server. However, in some scenarios, developers might want requests sent from the browser to bypass the SSR service and directly request the data source, for example: - -1. To reduce network consumption in the browser by directly requesting the data source. -2. If the application has data caching in the browser and doesn't want to request data from the SSR service. - -Module Federation supports adding an additional `.data.client` file in SSR applications, which also exports a named loader. In this case, if the Data Loader in the SSR application encounters an error and downgrades, or when switching routes in the browser, it will execute this loader function in the browser like a CSR application, instead of sending a data request to the SSR service. - -```ts title="List.data.client.ts" -import cache from 'my-cache'; - -export async function loader({ params }) { - if (cache.has(params.id)) { - return cache.get(params.id); - } - const res = await fetch('URL_ADDRESS?id={params.id}'); - return { - message: res.message, - } -} -``` - -:::warning WARNING -To use a Client Loader, there must be a corresponding Server Loader, and the Server Loader must be defined using the `.data` file convention, not the `.loader` file convention. -::: - -### FAQ - -#### Application-Level Data Fetching? - -For application-level modules, we prefer to use RSC (React Server Components) to achieve more complete functionality. This feature is currently being explored, so please stay tuned. - -#### Support nested remote? - -Not supported yet. +The Modern.js plugin re-exports `@module-federation/bridge-react` from `@module-federation/modern-js/react`, so you don't need to install it separately. ## API -In addition to exporting [MF Runtime](../basic/runtime/runtime-api), `@module-federation/modern-js/runtime` also provides a series of APIs to help developers better use Module Federation. - -To prevent conflicts with Shared modules, you need to import them as follows: - -```ts -import { kit } from '@module-federation/modern-js/runtime'; -const { loadRemote ,createRemoteComponent, createRemoteSSRComponent, wrapNoSSR } = kit; -``` - -### createRemoteComponent - -import Collapse from '@components/Collapse' - - -```ts -declare function createRemoteComponent( - props: CreateRemoteComponentOptions -): (props: ComponentType) => React.JSX.Element; - -type CreateRemoteComponentOptions = { - loader: () => Promise; - loading: React.ReactNode; - fallback: ReactNode | ((errorInfo: ErrorInfo) => ReactNode); - export?: E; - dataFetchParams?: DataFetchParams; -}; - -type ComponentType = T[E] extends (...args: any) => any - ? Parameters[0] extends undefined - ? Record - : Parameters[0] - : Record; - -type DataFetchParams = { - isDowngrade: boolean; -} & Record; - -type ErrorInfo = { - error: Error; - errorType: number; - dataFetchMapKey?: string; -}; -``` - - -This function supports loading components and also provides the following capabilities: - -1. In SSR mode, it injects the corresponding producer's style tags/script resources. This behavior helps avoid CSS flickering issues caused by stream rendering and accelerates PID (First Paint Interactive Time). -2. If the producer has a data fetching function, this function will be automatically called and the data injected. - -#### Example - -```tsx -import React, { FC, memo, useEffect } from 'react'; -import { kit, ERROR_TYPE } from '@module-federation/modern-js/runtime'; - -const { createRemoteComponent } = kit; - -const RemoteComponent = createRemoteComponent({ - loader: () => import('remote/Image'), - loading:
loading...
, - fallback: ({error,errorType,dataFetchMapKey}) => { - console.error(error) - if(errorType === ERROR_TYPE.LOAD_REMOTE){ - return
load remote failed
- } - if(errorType === ERROR_TYPE.DATA_FETCH){ - return
data fetch failed, the dataFetchMapKey key is: {dataFetchMapKey}
- } - return
error type is unknown
; - }, -}); - -const App: FC = () => { - return <> - - ; -}; -export default App; -``` - -#### loader - -- Type: `() => Promise` -- Required: Yes -- Default value: `undefined` - -A function to load the remote component, usually `()=>loadRemote(id)` or `()=>import(id)`. - -#### loading - -- Type: `React.ReactNode` -- Required: Yes -- Default value: `undefined` - -Sets the module loading state. - -#### fallback - -- Type: `(({ error }: { error: ErrorInfo}) => React.ReactElement)` -- Required: Yes -- Default value: `undefined` - -The fallback component rendered when the component fails to **load** or **render**. - -#### export - -- Type: `string` -- Required: No -- Default value: `'default'` - -If the remote component is a named export, you can use this parameter to specify the name of the component to export. By default, it loads the default export. - -#### dataFetchParams - -- Type: `DataFetchParams` -- Required: No -- Default value: `undefined` - -If the remote component has a data fetching function, setting this will pass it to the data fetching function. - -### createRemoteSSRComponent - -:::danger Note -This function will be deprecated. It is recommended to use [createRemoteComponent](#createremotecomponent). -::: - -### wrapNoSSR - - -```ts -declare function wrapNoSSR( - createComponentFn: typeof createRemoteComponent, -) : (options: CreateRemoteComponentOptions) => (props: ComponentType) => React.JSX.Element -``` - - -Wraps a component so that it does not render in SSR scenarios. - -Usage example: - -```tsx -import { kit } from '@module-federation/modern-js/runtime'; - -const { createRemoteComponent, wrapNoSSR } = kit; - -const RemoteComponent = wrapNoSSR(createRemoteComponent)({ - loader: () => { - return import('remote/Content'); - }, - loading: 'loading...', - export: 'default', - fallback: ({ error }) => { - if (error instanceof Error && error.message.includes('not exist')) { - return
fallback - not existed id
; - } - return
fallback
; - }, -}); - -const Index = (): JSX.Element => { - return ( -
-

- The component will be render in csr. -

- -
- ); -}; - -export default Index; - -``` +`@module-federation/modern-js` re-exports `@module-federation/modern-js/runtime` from the `runtime` subpath. You can use `@module-federation/modern-js/runtime` to get the MF Runtime.Runtime。 ## Configuration diff --git a/apps/website-new/docs/en/guide/troubleshooting/runtime/RUNTIME-008.mdx b/apps/website-new/docs/en/guide/troubleshooting/runtime/RUNTIME-008.mdx index a96cb6d655b..e0bbb01a2a8 100644 --- a/apps/website-new/docs/en/guide/troubleshooting/runtime/RUNTIME-008.mdx +++ b/apps/website-new/docs/en/guide/troubleshooting/runtime/RUNTIME-008.mdx @@ -9,4 +9,4 @@ Failed to load resource during the runtime process, which may be caused by netwo ## Solutions -check whether the resource address is correct. If the resource address is correct, check whether the network is stable. If the network is unstable, you can add the retry mechanism, refer to [Runtime Retry Mechanism](en/plugin/plugins/retry-plugin.html). \ No newline at end of file +check whether the resource address is correct. If the resource address is correct, check whether the network is stable. If the network is unstable, you can add the retry mechanism, refer to [Runtime Retry Mechanism](/plugin/plugins/retry-plugin.html). diff --git a/apps/website-new/docs/en/practice/bridge/index.mdx b/apps/website-new/docs/en/practice/bridge/index.mdx index f54930b89f1..b2dd3adc1d9 100644 --- a/apps/website-new/docs/en/practice/bridge/index.mdx +++ b/apps/website-new/docs/en/practice/bridge/index.mdx @@ -27,11 +27,13 @@ This toolkit provides two APIs: > Used to create React application-type module exports. If your application is React-based and you want it to be loaded as an application-type module by another MF application, you should use this to create standard-compliant exports for your application. -- createRemoteComponent +- createRemoteAppComponent + +> Used to load application-type modules in a React application. The loaded module must be wrapped by `createBridgeComponent`. `createRemoteAppComponent` will automatically create a rendering context in your application to ensure the module works properly. -> Used to load application-type modules in a React application. The loaded module must be wrapped by `createBridgeComponent`. `createRemoteComponent` will automatically create a rendering context in your application to ensure the module works properly. +- createRemoteComponent -For usage of `@module-federation/bridge-react`, see [Host demo](https://github.com/module-federation/core/tree/main/apps/router-demo/router-host-2000) and [Remote demo](https://github.com/module-federation/core/tree/main/apps/router-demo/router-remote2-2002). +> It will be deprecated, please use `createRemoteAppComponent` instead. ### @module-federation/bridge-vue3 @@ -43,9 +45,9 @@ This toolkit provides two APIs: > Used to create Vue application-type module exports. If your application is Vue v3-based and you want it to be loaded as an application-type module by another MF application, you should use this to create standard-compliant exports for your application. -- createRemoteComponent +- createRemoteAppComponent -> Used to load application-type modules in a Vue application. The loaded module must be wrapped by `createBridgeComponent`. `createRemoteComponent` will automatically create a rendering context in your application to ensure the module works properly. +> Used to load application-type modules in a Vue application. The loaded module must be wrapped by `createBridgeComponent`. `createRemoteAppComponent` will automatically create a rendering context in your application to ensure the module works properly. ## FAQ diff --git a/apps/website-new/docs/en/practice/bridge/react-bridge.mdx b/apps/website-new/docs/en/practice/bridge/react-bridge.mdx index 5f55234874f..a3cffb7d81b 100644 --- a/apps/website-new/docs/en/practice/bridge/react-bridge.mdx +++ b/apps/website-new/docs/en/practice/bridge/react-bridge.mdx @@ -2,9 +2,10 @@ `@module-federation/bridge-react` provides bridge utility functions for React applications: - `createBridgeComponent`: Used for exporting application-level modules, suitable for producers to wrap modules exported as application types -- `createRemoteComponent`: Used for loading application-level modules, suitable for consumers to load modules as application types +- `createRemoteAppComponent`: Used for loading application-level modules, suitable for consumers to load modules as application types +- `lazyLoadComponentPlugin`: Used for registering `createLazyComponent`、`prefetch` API -### React Version Compatibility +## React Version Compatibility `@module-federation/bridge-react` supports multiple major React versions, ensuring seamless integration between applications using different React versions: @@ -12,7 +13,7 @@ - **React 18**: Fully supported, using the new `createRoot` and `hydrateRoot` APIs - **React 19**: Fully supported, using `createRoot` and `hydrateRoot` APIs, but with legacy render methods completely removed -### Version-Specific Imports +## Version-Specific Imports You can choose specific import paths based on the React version used in your project: @@ -32,7 +33,7 @@ This multi-version compatibility allows different teams to independently choose [View Demo](https://github.com/module-federation/core/tree/main/apps/router-demo) -### Installation +## Installation import { PackageManagerTabs } from '@theme'; @@ -44,9 +45,9 @@ import { PackageManagerTabs } from '@theme'; }} /> -### Examples +## Examples -#### Exporting Application Type Modules +### Exporting Application Type Modules :::danger Note: After using `@module-federation/bridge-react`, you cannot set `react-router-dom` as shared, otherwise the build tool will throw an exception. This is because `@module-federation/bridge-react` implements route control by proxying `react-router-dom` to ensure that inter-application routing works correctly. @@ -97,7 +98,7 @@ Why do application type modules need to be wrapped with `createBridgeComponent`? ::: -#### Loading Application Type Modules +### Loading Application Type Modules > Host @@ -118,11 +119,11 @@ export default defineConfig({ }); ``` -- Step 2: In the host application, we need to use `createRemoteComponent` to load the remote application. +- Step 2: In the host application, we need to use `createRemoteAppComponent` to load the remote application. ```tsx // ./src/App.tsx -import { createRemoteComponent } from '@module-federation/bridge-react'; +import { createRemoteAppComponent } from '@module-federation/bridge-react'; import { loadRemote } from '@module-federation/runtime'; // Define FallbackError component @@ -141,8 +142,8 @@ const FallbackErrorComp = ({ error }: { error: Error }) => { // Define FallbackLoading component const FallbackComp =
loading...
; -// Use createRemoteComponent to create remote component -const Remote1App = createRemoteComponent({ +// Use createRemoteAppComponent to create remote component +const Remote1App = createRemoteAppComponent({ // loader is used to load remote modules, e.g.: loadRemote('remote1/export-app'), import('remote1/export-app') loader: () => loadRemote('remote1/export-app'), // fallback is used for displaying components when remote module loading fails @@ -177,17 +178,53 @@ const App = () => { :::info -1. The remote module exported by `createRemoteComponent` will automatically use the react-bridge loading protocol to load the module, +1. The remote module exported by `createRemoteAppComponent` will automatically use the react-bridge loading protocol to load the module, making cross-framework rendering of applications possible. -2. Additionally, `createRemoteComponent` will automatically handle module loading, module destruction, error handling, loading, routing, and other logic, +2. Additionally, `createRemoteAppComponent` will automatically handle module loading, module destruction, error handling, loading, routing, and other logic, allowing developers to focus solely on how to use the remote component. ::: -### API Reference -#### createBridgeComponent +### Register lazyLoadComponentPlugin + +Register the `lazyLoadComponentPlugin` at runtime to make the `createLazyComponent` and `prefetch` APIs available. + +```ts +import { init } from '@module-federation/runtime'; +import { lazyLoadComponentPlugin } from '@module-federation/bridge-react'; + +const instance = init({ + name: 'host', + remotes, + plugins: [lazyLoadComponentPlugin()] +}); + +``` +After registration, you can use the `createLazyComponent` and `prefetch` APIs. + +```ts +instance.prefetch({ + id: 'dynamic_remote' +}); + +const LazyComponent = instance.createLazyComponent({ + loader: () => loadRemote('dynamic_remote'), + loading: 'loading...', + fallback: ({ error }) => { + if (error instanceof Error && error.message.includes('not exist')) { + return
fallback - not existed id
; + } + return
fallback
; + }, +}); + +``` + +## API Reference + +### createBridgeComponent `createBridgeComponent` is used to export remote React components. @@ -210,8 +247,8 @@ function createBridgeComponent( interface ProviderFnParams { /** Root component to be remotely loaded */ rootComponent: React.ComponentType; - - /** + + /** * Custom render function for custom rendering logic * @param App - React element * @param id - DOM element or string ID @@ -221,8 +258,8 @@ interface ProviderFnParams { App: React.ReactElement, id?: HTMLElement | string, ) => RootType | Promise; - - /** + + /** * Custom createRoot function for React 18 and above * @param container - DOM container * @param options - CreateRoot options @@ -232,7 +269,7 @@ interface ProviderFnParams { container: Element | DocumentFragment, options?: CreateRootOptions, ) => Root; - + /** * Default options for createRoot in React 18 and 19 * These options will be used when creating a root unless overridden by rootOptions in render params @@ -246,9 +283,9 @@ interface ProviderFnParams { } ``` -#### createRemoteComponent +### createRemoteAppComponent -`createRemoteComponent` is used to load remote React components. +`createRemoteAppComponent` is used to load remote React components. ```tsx /** @@ -256,7 +293,7 @@ interface ProviderFnParams { * @param options - Remote component configuration options * @returns Returns a React component that can receive props and render remote component */ -function createRemoteComponent( +function createRemoteAppComponent( options: RemoteComponentParams ): React.ForwardRefExoticComponent< React.PropsWithoutRef & React.RefAttributes @@ -269,19 +306,19 @@ interface RemoteComponentParams< T = Record, E extends keyof T = keyof T > { - /** + /** * Function to load remote module * Example: () => loadRemote('remote1/export-app') or () => import('remote1/export-app') */ loader: () => Promise; - + /** Component displayed when loading remote module */ loading: React.ReactNode; - + /** Error component displayed when loading or rendering remote module fails */ fallback: React.ComponentType<{ error: Error }>; - - /** + + /** * Specify module export name * Default is 'default' */ @@ -295,26 +332,234 @@ interface RemoteComponentProps> { /** Properties passed to remote component */ props?: T; - /** + /** * Memory route configuration, used to control child application routing as memoryRouter * Will not directly display URL in browser address bar */ memoryRoute?: { entryPath: string }; - + /** Base path name */ basename?: string; - + /** Style */ style?: React.CSSProperties; - + /** Class name */ className?: string; } ``` -### Usage Examples -#### Using export to specify module export +### createLazyComponent + +> This API can only be called after the [lazyLoadComponentPlugin is registered](#register-lazyloadcomponentplugin). + +import Collapse from '@components/Collapse' + + +```ts +declare function createLazyComponent( + props: CreateLazyComponentOptions +): (props: ComponentType) => React.JSX.Element; + +type CreateLazyComponentOptions = { + loader: () => Promise; + loading: React.ReactNode; + delayLoading?: number; + fallback: ReactNode | ((errorInfo: ErrorInfo) => ReactNode); + export?: E; + dataFetchParams?: DataFetchParams; + noSSR?: boolean; + injectScript?: boolean; + injectLink?: boolean; +}; + +type ComponentType = T[E] extends (...args: any) => any + ? Parameters[0] extends undefined + ? Record + : Parameters[0] + : Record; + +type DataFetchParams = { + isDowngrade: boolean; +} & Record; + +type ErrorInfo = { + error: Error; + errorType: number; + dataFetchMapKey?: string; +}; +``` + + +In addition to loading components, this function also supports the following capabilities: + +1. In SSR mode, it injects the corresponding producer's style tags/script resources. This helps to avoid CSS flickering issues caused by streaming rendering and accelerates the Time to Interactive (TTI). +2. If the producer has a data fetch function, this function will be called automatically and the data will be injected. + +```tsx +import React, { FC, memo, useEffect } from 'react'; +import { getInstance } from '@module-federation/enhanced/runtime'; +import { createLazyComponent, ERROR_TYPE } from '@module-federation/bridge-react'; + +const instance = getInstance(); +const LazyComponent = instance.createLazyComponent({ + loader: () => import('remote/Image'), + loading:
loading...
, + fallback: ({error,errorType,dataFetchMapKey}) => { + console.error(error) + if(errorType === ERROR_TYPE.LOAD_REMOTE){ + return
load remote failed
+ } + if(errorType === ERROR_TYPE.DATA_FETCH){ + return
data fetch failed, the dataFetchMapKey key is: {dataFetchMapKey}
+ } + return
error type is unknown
; + }, +}); + +const App: FC = () => { + return <> + + ; +}; +export default App; +``` + +#### loader + +- Type: `() => Promise` +- Required: Yes +- Default: `undefined` + +A function to load the remote component, usually `()=>loadRemote(id)` or `()=>import(id)`. + +#### loading + +- Type: `React.ReactNode` +- Required: Yes +- Default: `undefined` + +Sets the loading state of the module. + +#### delayLoading + +- Type: `number` +- Required: No +- Default: `undefined` + +Sets the delay time for showing the loading state, in milliseconds. If the loading time is less than this value, the loading state will not be displayed. + +#### fallback + +- Type: `(({ error }: { error: ErrorInfo}) => React.ReactElement)` +- Required: Yes +- Default: `undefined` + +The fallback component to render when the component fails to **load** or **render**. + +#### export + +- Type: `string` +- Required: No +- Default: `'default'` + +If the remote component is a named export, this parameter can be used to specify the name of the component to be exported. By default, it loads the default export. + +#### dataFetchParams + +- Type: `DataFetchParams` +- Required: No +- Default: `undefined` + +If the remote component has a data fetch function, this object will be passed to it. + +#### noSSR + +- Type: `boolean` +- Required: No +- Default: `false` + +If set to `true`, this component will not be rendered in an SSR environment. + +#### injectScript + +- Type: `boolean` +- Required: No +- Default: `false` + +In an SSR environment, if set to `true`, the created component will have its corresponding script resource injected. + +For example, if `remote/button` has `__federation_button.js`, the corresponding script will be injected before the component in the HTML returned by SSR to speed up interactivity. + +```html +