From f56497f694e870ea8be3e0189e62d35bcafc8ba6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=B0=B4=E6=BE=9C?=
Date: Tue, 25 Apr 2023 10:47:40 +0800
Subject: [PATCH] feat: async data loader (#6137)
* chore: refactor unfinished
* feat: support create router
* refactor: render mode
* fix: code splitting false
* feat: add location for icestark
* chore: remove console
* test: examples
* fix: dataloader is undefined
* fix: test
* fix: test case
* fix: test case
* fix: types
* fix: test case
* fix: lock
* feat: async data loader
* fix: update lock
* fix: hydration
* fix: router
* fix: router
* chore: log
* fix: hmr
* fix: test
* fix: test
* feat: await
* fix: await component
* fix: lint
* refactor: type
* fix: type
* fix: app data loader
* fix: test
* fix: test
* test: async data
* test: async data
* docs: async data loader
* fix: lint
* refactor: loader config
* fix: test
* fix: compat with old useage
---------
Co-authored-by: ClarkXia
---
.../src/pages/with-defer-loader.tsx | 35 +++++
.../src/pages/with-defer-loaders.tsx | 52 ++++++++
.../with-data-loader/src/pages/with-ssr.tsx | 45 +++++++
packages/ice/src/constant.ts | 1 +
packages/runtime/src/appData.ts | 16 ++-
packages/runtime/src/dataLoader.ts | 86 ++++++-------
packages/runtime/src/index.ts | 8 +-
packages/runtime/src/routes.tsx | 121 +++++++++++++++---
packages/runtime/src/types.ts | 13 +-
packages/runtime/tests/routes.test.tsx | 26 +++-
packages/runtime/tests/runClientApp.test.tsx | 26 ++--
packages/runtime/tests/runServerApp.test.tsx | 4 +-
pnpm-lock.yaml | 32 +++--
tests/integration/with-data-loader.test.ts | 8 ++
website/docs/guide/basic/data-loader.md | 87 ++++++++++++-
15 files changed, 455 insertions(+), 105 deletions(-)
create mode 100644 examples/with-data-loader/src/pages/with-defer-loader.tsx
create mode 100644 examples/with-data-loader/src/pages/with-defer-loaders.tsx
create mode 100644 examples/with-data-loader/src/pages/with-ssr.tsx
diff --git a/examples/with-data-loader/src/pages/with-defer-loader.tsx b/examples/with-data-loader/src/pages/with-defer-loader.tsx
new file mode 100644
index 000000000..0396facb9
--- /dev/null
+++ b/examples/with-data-loader/src/pages/with-defer-loader.tsx
@@ -0,0 +1,35 @@
+import { useData, defineDataLoader, Await } from 'ice';
+import styles from './index.module.css';
+
+export default function Home() {
+ const data = useData();
+
+ return (
+ <>
+ With dataLoader
+ Loading item info...
} errorElement={Error loading!
}>
+ {(itemInfo) => {
+ return Item id is {itemInfo.id}
;
+ }}
+
+ >
+ );
+}
+
+export function pageConfig() {
+ return {
+ title: 'Home',
+ };
+}
+
+export const dataLoader = defineDataLoader(async () => {
+ const promise = new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({
+ id: 1233,
+ });
+ }, 100);
+ });
+ return await promise;
+}, { defer: true });
+
diff --git a/examples/with-data-loader/src/pages/with-defer-loaders.tsx b/examples/with-data-loader/src/pages/with-defer-loaders.tsx
new file mode 100644
index 000000000..06f7eb5cd
--- /dev/null
+++ b/examples/with-data-loader/src/pages/with-defer-loaders.tsx
@@ -0,0 +1,52 @@
+import { useData, defineDataLoader, Await } from 'ice';
+import styles from './index.module.css';
+
+export default function Home() {
+ const data = useData();
+
+ return (
+ <>
+ With dataLoader
+ Loading item info...} errorElement={Error loading!
}>
+ {(itemInfo) => {
+ return Item id is {itemInfo.id}
;
+ }}
+
+ Loading item info...} errorElement={Error loading!
}>
+ {(itemInfo) => {
+ return Item price is {itemInfo.price}
;
+ }}
+
+ >
+ );
+}
+
+export function pageConfig() {
+ return {
+ title: 'Home',
+ };
+}
+
+export const dataLoader = defineDataLoader([
+ async () => {
+ const promise = new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({
+ id: 1233,
+ });
+ }, 100);
+ });
+ return await promise;
+ },
+ async () => {
+ const promise = new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({
+ price: 9.99,
+ });
+ }, 2000);
+ });
+ return await promise;
+ },
+], { defer: true });
+
diff --git a/examples/with-data-loader/src/pages/with-ssr.tsx b/examples/with-data-loader/src/pages/with-ssr.tsx
new file mode 100644
index 000000000..16c363547
--- /dev/null
+++ b/examples/with-data-loader/src/pages/with-ssr.tsx
@@ -0,0 +1,45 @@
+import { useData, defineDataLoader, defineServerDataLoader, Await } from 'ice';
+import styles from './index.module.css';
+
+export default function Home() {
+ const data = useData();
+
+ return (
+ <>
+ With dataLoader
+ Loading item info...} errorElement={Error loading!
}>
+ {(itemInfo) => {
+ return Item id is {itemInfo.id}
;
+ }}
+
+ >
+ );
+}
+
+export function pageConfig() {
+ return {
+ title: 'Home',
+ };
+}
+
+export const dataLoader = defineDataLoader(async () => {
+ const promise = new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({
+ id: 1233,
+ });
+ }, 100);
+ });
+ return await promise;
+}, { defer: true });
+
+export const serverDataLoader = defineServerDataLoader(async () => {
+ const promise = new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({
+ id: 1233,
+ });
+ }, 100);
+ });
+ return await promise;
+});
\ No newline at end of file
diff --git a/packages/ice/src/constant.ts b/packages/ice/src/constant.ts
index caeb9b6b1..ecb0e9d1b 100644
--- a/packages/ice/src/constant.ts
+++ b/packages/ice/src/constant.ts
@@ -61,6 +61,7 @@ export const RUNTIME_EXPORTS = [
'ClientOnly',
'withSuspense',
'useSuspenseData',
+ 'Await',
'defineDataLoader',
'defineServerDataLoader',
'defineStaticDataLoader',
diff --git a/packages/runtime/src/appData.ts b/packages/runtime/src/appData.ts
index 351b3a6ad..57fb28e2f 100644
--- a/packages/runtime/src/appData.ts
+++ b/packages/runtime/src/appData.ts
@@ -12,15 +12,21 @@ async function getAppData(appExport: AppExport, requestContext?: RequestContext)
return await globalLoader.getData('__app');
}
- if (appExport?.dataLoader) {
- return await appExport.dataLoader(requestContext);
+ const appDataLoaderConfig = appExport?.dataLoader;
+
+ if (!appDataLoaderConfig) {
+ return null;
}
- const loader = appExport?.dataLoader;
+ let loader;
- if (!loader) return null;
+ if (typeof appDataLoaderConfig === 'function' || Array.isArray(appDataLoaderConfig)) {
+ loader = appDataLoaderConfig;
+ } else {
+ loader = appDataLoaderConfig.loader;
+ }
- await callDataLoader(loader, requestContext);
+ return await callDataLoader(loader, requestContext);
}
export {
diff --git a/packages/runtime/src/dataLoader.ts b/packages/runtime/src/dataLoader.ts
index dcb80936b..ffb1d2913 100644
--- a/packages/runtime/src/dataLoader.ts
+++ b/packages/runtime/src/dataLoader.ts
@@ -1,16 +1,18 @@
-import type { RequestContext, RenderMode, DataLoaderConfig, DataLoaderResult, RuntimeModules, AppExport, StaticRuntimePlugin, CommonJsRuntime, StaticDataLoader } from './types.js';
import getRequestContext from './requestContext.js';
-
+import type {
+ RequestContext, RenderMode, AppExport,
+ RuntimeModules, StaticRuntimePlugin, CommonJsRuntime,
+ Loader, DataLoaderResult, StaticDataLoader, DataLoaderConfig, DataLoaderOptions,
+} from './types.js';
interface Loaders {
[routeId: string]: DataLoaderConfig;
}
interface CachedResult {
value: any;
- status: string;
}
-interface LoaderOptions {
+interface Options {
fetcher: Function;
runtimeModules: RuntimeModules['statics'];
appExport: AppExport;
@@ -20,16 +22,24 @@ export interface LoadRoutesDataOptions {
renderMode: RenderMode;
}
-export function defineDataLoader(dataLoaderConfig: DataLoaderConfig): DataLoaderConfig {
- return dataLoaderConfig;
+export function defineDataLoader(dataLoader: Loader, options?: DataLoaderOptions): DataLoaderConfig {
+ return {
+ loader: dataLoader,
+ options,
+ };
}
-export function defineServerDataLoader(dataLoaderConfig: DataLoaderConfig): DataLoaderConfig {
- return dataLoaderConfig;
+export function defineServerDataLoader(dataLoader: Loader, options?: DataLoaderOptions): DataLoaderConfig {
+ return {
+ loader: dataLoader,
+ options,
+ };
}
-export function defineStaticDataLoader(dataLoaderConfig: DataLoaderConfig): DataLoaderConfig {
- return dataLoaderConfig;
+export function defineStaticDataLoader(dataLoader: Loader): DataLoaderConfig {
+ return {
+ loader: dataLoader,
+ };
}
/**
@@ -124,12 +134,13 @@ export function loadDataByCustomFetcher(config: StaticDataLoader) {
/**
* Handle for different dataLoader.
*/
-export function callDataLoader(dataLoader: DataLoaderConfig, requestContext: RequestContext): DataLoaderResult {
+export function callDataLoader(dataLoader: Loader, requestContext: RequestContext): DataLoaderResult {
if (Array.isArray(dataLoader)) {
const loaders = dataLoader.map(loader => {
return typeof loader === 'object' ? loadDataByCustomFetcher(loader) : loader(requestContext);
});
- return Promise.all(loaders);
+
+ return loaders;
}
if (typeof dataLoader === 'object') {
@@ -156,7 +167,6 @@ function loadInitialDataInClient(loaders: Loaders) {
if (dataFromSSR) {
cache.set(renderMode === 'SSG' ? `${id}_ssg` : id, {
value: dataFromSSR,
- status: 'RESOLVED',
});
if (renderMode === 'SSR') {
@@ -164,15 +174,15 @@ function loadInitialDataInClient(loaders: Loaders) {
}
}
- const dataLoader = loaders[id];
+ const dataLoaderConfig = loaders[id];
- if (dataLoader) {
+ if (dataLoaderConfig) {
const requestContext = getRequestContext(window.location);
- const loader = callDataLoader(dataLoader, requestContext);
+ const { loader } = dataLoaderConfig;
+ const promise = callDataLoader(loader, requestContext);
cache.set(id, {
- value: loader,
- status: 'LOADING',
+ value: promise,
});
}
});
@@ -183,7 +193,7 @@ function loadInitialDataInClient(loaders: Loaders) {
* Load initial data and register global loader.
* In order to load data, JavaScript modules, CSS and other assets in parallel.
*/
-async function init(dataloaderConfig: Loaders, options: LoaderOptions) {
+async function init(loaders: Loaders, options: Options) {
const {
fetcher,
runtimeModules,
@@ -208,57 +218,39 @@ async function init(dataloaderConfig: Loaders, options: LoaderOptions) {
}
try {
- loadInitialDataInClient(dataloaderConfig);
+ loadInitialDataInClient(loaders);
} catch (error) {
console.error('Load initial data error: ', error);
}
(window as any).__ICE_DATA_LOADER__ = {
- getData: async (id, options: LoadRoutesDataOptions) => {
+ getData: (id, options: LoadRoutesDataOptions) => {
let result;
- // first render for ssg use data from build time.
- // second render for ssg will use data from data loader.
+ // First render for ssg use data from build time, second render for ssg will use data from data loader.
const cacheKey = `${id}${options?.renderMode === 'SSG' ? '_ssg' : ''}`;
+
+ // In CSR, all dataLoader is called by global data loader to avoid bundle dataLoader in page bundle duplicate.
result = cache.get(cacheKey);
// Always fetch new data after cache is been used.
cache.delete(cacheKey);
// Already send data request.
if (result) {
- const { status, value } = result;
-
- if (status === 'RESOLVED') {
- return result;
- }
-
- try {
- if (Array.isArray(value)) {
- return await Promise.all(value);
- }
-
- return await value;
- } catch (error) {
- console.error('DataLoader: getData error.\n', error);
-
- return {
- message: 'DataLoader: getData error.',
- error,
- };
- }
+ return result.value;
}
- const dataLoader = dataloaderConfig[id];
+ const dataLoaderConfig = loaders[id];
// No data loader.
- if (!dataLoader) {
+ if (!dataLoaderConfig) {
return null;
}
// Call dataLoader.
- // In CSR, all dataLoader is called by global data loader to avoid bundle dataLoader in page bundle duplicate.
const requestContext = getRequestContext(window.location);
- return await callDataLoader(dataLoader, requestContext);
+ const { loader } = dataLoaderConfig;
+ return callDataLoader(loader, requestContext);
},
};
}
diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts
index 879bdc5f7..38991adb3 100644
--- a/packages/runtime/src/index.ts
+++ b/packages/runtime/src/index.ts
@@ -16,7 +16,7 @@ import type {
RouteWrapper,
RenderMode,
DistType,
- DataLoaderConfig,
+ Loader,
RouteWrapperConfig,
} from './types.js';
import Runtime from './runtime.js';
@@ -50,7 +50,7 @@ import KeepAliveOutlet from './KeepAliveOutlet.js';
import ClientOnly from './ClientOnly.js';
import useMounted from './useMounted.js';
import { withSuspense, useSuspenseData } from './Suspense.js';
-import { createRouteLoader, WrapRouteComponent, RouteErrorComponent } from './routes.js';
+import { createRouteLoader, WrapRouteComponent, RouteErrorComponent, Await } from './routes.js';
export {
getAppConfig,
@@ -92,6 +92,8 @@ export {
withSuspense,
useSuspenseData,
+ Await,
+
createRouteLoader,
WrapRouteComponent,
RouteErrorComponent,
@@ -109,7 +111,7 @@ export type {
RouteWrapper,
RenderMode,
DistType,
- DataLoaderConfig,
+ Loader,
RunClientAppOptions,
MetaType,
TitleType,
diff --git a/packages/runtime/src/routes.tsx b/packages/runtime/src/routes.tsx
index 46db33fa5..83db91842 100644
--- a/packages/runtime/src/routes.tsx
+++ b/packages/runtime/src/routes.tsx
@@ -1,6 +1,8 @@
-import React from 'react';
-import { useRouteError } from 'react-router-dom';
-import type { RouteItem, RouteModules, RenderMode, DataLoaderConfig, RequestContext, ComponentModule } from './types.js';
+import React, { Suspense } from 'react';
+import { useRouteError, defer, Await as ReactRouterAwait } from 'react-router-dom';
+// eslint-disable-next-line camelcase
+import type { UNSAFE_DeferredData } from '@remix-run/router';
+import type { RouteItem, RouteModules, RenderMode, RequestContext, ComponentModule, DataLoaderConfig } from './types.js';
import RouteWrapper from './RouteWrapper.js';
import { useAppContext } from './AppContext.js';
import { callDataLoader } from './dataLoader.js';
@@ -89,12 +91,25 @@ export function RouteErrorComponent() {
return <>>;
}
+export function Await(props) {
+ return (
+
+
+ {props.children}
+
+
+ );
+}
+
/**
* Create loader function for route module.
*/
interface LoaderData {
- data: any;
- pageConfig: any;
+ data?: any;
+ pageConfig?: any;
}
export interface RouteLoaderOptions {
@@ -104,28 +119,94 @@ export interface RouteLoaderOptions {
renderMode: RenderMode;
}
-export function createRouteLoader(options: RouteLoaderOptions): () => Promise {
- return async () => {
- const { dataLoader, pageConfig, staticDataLoader, serverDataLoader } = options.module;
- const { requestContext, renderMode, routeId } = options;
+// eslint-disable-next-line camelcase
+type LoaderFunction = () => LoaderData | UNSAFE_DeferredData | Promise;
+
+export function createRouteLoader(options: RouteLoaderOptions): LoaderFunction {
+ const { dataLoader, pageConfig, staticDataLoader, serverDataLoader } = options.module;
+ const { requestContext, renderMode, routeId } = options;
+
+ let dataLoaderConfig: DataLoaderConfig;
+ if (renderMode === 'SSG') {
+ dataLoaderConfig = staticDataLoader;
+ } else if (renderMode === 'SSR') {
+ dataLoaderConfig = serverDataLoader || dataLoader;
+ } else {
+ dataLoaderConfig = dataLoader;
+ }
+
+ if (!dataLoaderConfig) {
+ return () => {
+ return {
+ pageConfig: pageConfig ? pageConfig({}) : {},
+ };
+ };
+ }
+
+ let loader;
+ let loaderOptions;
+
+ // Compat dataLoaderConfig not return by defineDataLoader.
+ if (typeof dataLoaderConfig === 'function' || Array.isArray(dataLoaderConfig)) {
+ loader = dataLoaderConfig;
+ } else {
+ loader = dataLoaderConfig.loader;
+ loaderOptions = dataLoaderConfig.options;
+ }
+
+ const getData = () => {
const hasGlobalLoader = typeof window !== 'undefined' && (window as any).__ICE_DATA_LOADER__;
const globalLoader = hasGlobalLoader ? (window as any).__ICE_DATA_LOADER__ : null;
let routeData: any;
if (globalLoader) {
- routeData = await globalLoader.getData(routeId, { renderMode });
+ routeData = globalLoader.getData(routeId, { renderMode });
} else {
- let loader: DataLoaderConfig;
- if (renderMode === 'SSG') {
- loader = staticDataLoader;
- } else if (renderMode === 'SSR') {
- loader = serverDataLoader || dataLoader;
- } else {
- loader = dataLoader;
- }
- routeData = loader && await callDataLoader(loader, requestContext);
+ routeData = callDataLoader(loader, requestContext);
}
+ return routeData;
+ };
+
+ // Async dataLoader.
+ if (loaderOptions?.defer) {
+ return async () => {
+ const promise = getData();
+
+ return defer({
+ data: promise,
+ // Call pageConfig without data.
+ pageConfig: pageConfig ? pageConfig({}) : {},
+ });
+ };
+ }
+
+ // Await dataLoader before render.
+ return async () => {
+ const result = getData();
+
+ let routeData;
+ try {
+ if (Array.isArray(result)) {
+ routeData = await Promise.all(result);
+ } else if (result instanceof Promise) {
+ routeData = await result;
+ } else {
+ routeData = result;
+ }
+ } catch (error) {
+ console.error('DataLoader: getData error.\n', error);
+
+ routeData = {
+ message: 'DataLoader: getData error.',
+ error,
+ };
+ }
+
const routeConfig = pageConfig ? pageConfig({ data: routeData }) : {};
- const loaderData = { data: routeData, pageConfig: routeConfig };
+ const loaderData = {
+ data: routeData,
+ pageConfig: routeConfig,
+ };
+
// CSR and load next route data.
if (typeof window !== 'undefined') {
await updateRoutesConfig(loaderData);
diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts
index c1250b114..b4c03ed90 100644
--- a/packages/runtime/src/types.ts
+++ b/packages/runtime/src/types.ts
@@ -31,7 +31,7 @@ export type RouteConfig = T & {
export interface AppExport {
default?: AppConfig;
[key: string]: any;
- dataLoader?: DataLoader;
+ dataLoader?: DataLoaderConfig;
}
export type DataLoaderResult = (Promise | RouteData) | RouteData;
@@ -49,7 +49,7 @@ export interface StaticDataLoader {
// route.defineDataLoader
// route.defineServerDataLoader
// route.defineStaticDataLoader
-export type DataLoaderConfig = DataLoader | StaticDataLoader | Array;
+export type Loader = DataLoader | StaticDataLoader | Array;
// route.pageConfig
export type PageConfig = (args: { data?: RouteData }) => RouteConfig;
@@ -71,6 +71,15 @@ export interface RoutesData {
[routeId: string]: RouteData;
}
+export interface DataLoaderOptions {
+ defer?: boolean;
+}
+
+export interface DataLoaderConfig {
+ loader: Loader;
+ options?: DataLoaderOptions;
+}
+
export interface LoadersData {
[routeId: string]: LoaderData;
}
diff --git a/packages/runtime/tests/routes.test.tsx b/packages/runtime/tests/routes.test.tsx
index 5012d87bc..abcde3b6d 100644
--- a/packages/runtime/tests/routes.test.tsx
+++ b/packages/runtime/tests/routes.test.tsx
@@ -28,14 +28,20 @@ describe('routes', () => {
const homeItem = {
default: () => <>>,
pageConfig: () => ({ title: 'home' }),
- dataLoader: async () => ({ type: 'getData' }),
- serverDataLoader: async () => ({ type: 'getServerData' }),
- staticDataLoader: async () => ({ type: 'getStaticData' }),
+ dataLoader: { loader: async () => ({ type: 'getData' }) },
+ serverDataLoader: { loader: async () => ({ type: 'getServerData' }) },
+ staticDataLoader: { loader: async () => ({ type: 'getStaticData' }) },
};
const aboutItem = {
default: () => <>>,
pageConfig: () => ({ title: 'about' }),
};
+ const InfoItem = {
+ default: () => <>>,
+ pageConfig: () => ({ title: 'home' }),
+ dataLoader: { loader: async () => ({ type: 'getAsyncData' }), options: { defer: true } },
+ };
+
const homeLazyItem = {
Component: homeItem.default,
loader: createRouteLoader({
@@ -145,6 +151,19 @@ describe('routes', () => {
});
});
+ it('load async route', async () => {
+ const { data: deferredResult } = await createRouteLoader({
+ routeId: 'home',
+ module: InfoItem,
+ })();
+
+ const data = await deferredResult.data;
+
+ expect(data).toStrictEqual({
+ type: 'getAsyncData',
+ });
+ });
+
it('load route data for SSG', async () => {
const routesDataSSG = await createRouteLoader({
routeId: 'home',
@@ -226,7 +245,6 @@ describe('routes', () => {
})();
expect(routesDataCSR).toStrictEqual({
- data: undefined,
pageConfig: {
title: 'about',
},
diff --git a/packages/runtime/tests/runClientApp.test.tsx b/packages/runtime/tests/runClientApp.test.tsx
index c2658b547..63c889813 100644
--- a/packages/runtime/tests/runClientApp.test.tsx
+++ b/packages/runtime/tests/runClientApp.test.tsx
@@ -92,7 +92,9 @@ describe('run client app', () => {
);
},
pageConfig: () => ({ title: 'home' }),
- dataLoader: async () => ({ data: 'test' }),
+ dataLoader: {
+ loader: async () => ({ data: 'test' }),
+ },
};
const basicRoutes = [
{
@@ -110,8 +112,10 @@ describe('run client app', () => {
it('run with static runtime', async () => {
await runClientApp({
app: {
- dataLoader: async () => {
- return { msg: staticMsg };
+ dataLoader: {
+ loader: async () => {
+ return { msg: staticMsg };
+ },
},
},
// @ts-ignore don't need to pass params in test case.
@@ -261,9 +265,11 @@ describe('run client app', () => {
let executed = false;
await runClientApp({
app: {
- dataLoader: async () => {
- executed = true;
- return { msg: '-getAppData' };
+ dataLoader: {
+ loader: async () => {
+ executed = true;
+ return { msg: '-getAppData' };
+ },
},
},
// @ts-ignore don't need to pass params in test case.
@@ -290,9 +296,11 @@ describe('run client app', () => {
await runClientApp({
app: {
- dataLoader: async () => {
- executed = true;
- return { msg: 'app' };
+ dataLoader: {
+ loader: async () => {
+ executed = true;
+ return { msg: 'app' };
+ },
},
},
// @ts-ignore don't need to pass params in test case.
diff --git a/packages/runtime/tests/runServerApp.test.tsx b/packages/runtime/tests/runServerApp.test.tsx
index bfad032c4..2ef60ee19 100644
--- a/packages/runtime/tests/runServerApp.test.tsx
+++ b/packages/runtime/tests/runServerApp.test.tsx
@@ -14,7 +14,9 @@ describe('run server app', () => {
const homeItem = {
default: () => home
,
pageConfig: () => ({ title: 'home' }),
- dataLoader: async () => ({ data: 'test' }),
+ dataLoader: {
+ loader: async () => ({ data: 'test' }),
+ },
};
const basicRoutes = [
{
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c8dac2cef..4fba48243 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7457,6 +7457,7 @@ packages:
'@types/prop-types': 15.7.5
'@types/scheduler': 0.16.2
csstype: 3.1.1
+ dev: true
/@types/react/18.0.28:
resolution: {integrity: sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==}
@@ -9622,8 +9623,8 @@ packages:
engines: {node: '>=10'}
hasBin: true
dependencies:
- JSONStream: 1.3.5
is-text-path: 1.0.1
+ JSONStream: 1.3.5
lodash: 4.17.21
meow: 8.1.2
split2: 3.2.2
@@ -9653,6 +9654,7 @@ packages:
resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==}
dependencies:
is-what: 3.14.1
+ dev: false
/copy-text-to-clipboard/3.0.1:
resolution: {integrity: sha512-rvVsHrpFcL4F2P8ihsoLdFHmd404+CMg71S756oRSeQgqk51U3kicGdnvfkrxva0xXH92SjGS62B0XIJsbh+9Q==}
@@ -10752,6 +10754,7 @@ packages:
requiresBuild: true
dependencies:
prr: 1.0.1
+ dev: false
optional: true
/error-ex/1.3.2:
@@ -13065,6 +13068,7 @@ packages:
engines: {node: '>=0.10.0'}
hasBin: true
requiresBuild: true
+ dev: false
optional: true
/image-size/1.0.2:
@@ -13589,6 +13593,7 @@ packages:
/is-what/3.14.1:
resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==}
+ dev: false
/is-whitespace-character/1.0.4:
resolution: {integrity: sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==}
@@ -14870,6 +14875,7 @@ packages:
source-map: 0.6.1
transitivePeerDependencies:
- supports-color
+ dev: false
/leven/3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
@@ -15168,6 +15174,7 @@ packages:
dependencies:
pify: 4.0.1
semver: 5.7.1
+ dev: false
optional: true
/make-dir/3.1.0:
@@ -15554,6 +15561,7 @@ packages:
sax: 1.2.4
transitivePeerDependencies:
- supports-color
+ dev: false
optional: true
/negotiator/0.6.3:
@@ -16006,6 +16014,7 @@ packages:
/parse-node-version/1.0.1:
resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==}
engines: {node: '>= 0.10'}
+ dev: false
/parse-numeric-range/1.3.0:
resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==}
@@ -17337,6 +17346,7 @@ packages:
nanoid: 3.3.4
picocolors: 1.0.0
source-map-js: 1.0.2
+ dev: false
/postcss/8.4.21:
resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==}
@@ -17490,6 +17500,7 @@ packages:
/prr/1.0.1:
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
+ dev: false
optional: true
/pseudomap/1.0.2:
@@ -18624,12 +18635,6 @@ packages:
/react-dev-utils/12.0.1_a37q6j7dwawz22saey2vgkpwqm:
resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==}
engines: {node: '>=14'}
- peerDependencies:
- typescript: '>=2.7'
- webpack: '>=4'
- peerDependenciesMeta:
- typescript:
- optional: true
dependencies:
'@babel/code-frame': 7.18.6
address: 1.2.2
@@ -18660,7 +18665,9 @@ packages:
transitivePeerDependencies:
- eslint
- supports-color
+ - typescript
- vue-template-compiler
+ - webpack
/react-dom/17.0.2_react@17.0.2:
resolution: {integrity: sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==}
@@ -18671,6 +18678,7 @@ packages:
object-assign: 4.1.1
react: 17.0.2
scheduler: 0.20.2
+ dev: false
/react-dom/18.2.0_react@18.2.0:
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
@@ -18806,6 +18814,7 @@ packages:
/react-refresh/0.14.0:
resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==}
engines: {node: '>=0.10.0'}
+ dev: false
/react-router-config/5.1.1_2dl5roaqnyqqppnjni7uetnb3a:
resolution: {integrity: sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==}
@@ -18903,6 +18912,7 @@ packages:
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
+ dev: false
/react/18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
@@ -19518,6 +19528,7 @@ packages:
chokidar: 3.5.3
immutable: 4.2.4
source-map-js: 1.0.2
+ dev: false
/sax/1.2.4:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
@@ -19534,6 +19545,7 @@ packages:
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
+ dev: false
/scheduler/0.21.0:
resolution: {integrity: sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==}
@@ -20563,6 +20575,7 @@ packages:
serialize-javascript: 6.0.1
terser: 5.14.2
webpack: 5.76.2_w34or7orauknzckzea4nxxqrru
+ dev: true
/terser-webpack-plugin/5.3.5_webpack@5.76.2:
resolution: {integrity: sha512-AOEDLDxD2zylUGf/wxHxklEkOe2/r+seuyOWujejFrIxHf11brA1/dWQNIgXa1c6/Wkxgu7zvv0JhOWfc2ELEA==}
@@ -21810,7 +21823,7 @@ packages:
mime-types: 2.1.35
range-parser: 1.2.1
schema-utils: 4.0.0
- webpack: 5.76.2_w34or7orauknzckzea4nxxqrru
+ webpack: 5.76.2_esbuild@0.17.16
/webpack-dev-server/4.11.1_webpack@5.76.2:
resolution: {integrity: sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==}
@@ -21850,7 +21863,7 @@ packages:
serve-index: 1.9.1
sockjs: 0.3.24
spdy: 4.0.2
- webpack: 5.76.2_w34or7orauknzckzea4nxxqrru
+ webpack: 5.76.2_esbuild@0.17.16
webpack-dev-middleware: 5.3.3_webpack@5.76.2
ws: 8.12.1
transitivePeerDependencies:
@@ -22151,6 +22164,7 @@ packages:
- '@swc/core'
- esbuild
- uglify-js
+ dev: true
/webpackbar/5.0.2_webpack@5.76.2:
resolution: {integrity: sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==}
diff --git a/tests/integration/with-data-loader.test.ts b/tests/integration/with-data-loader.test.ts
index b5ae58132..1449443f0 100644
--- a/tests/integration/with-data-loader.test.ts
+++ b/tests/integration/with-data-loader.test.ts
@@ -49,6 +49,14 @@ describe(`start ${example}`, () => {
expect(timeStampForRouter3).not.toStrictEqual(timeStampForRouter1);
});
+ test('should work with deferred data loader', async () => {
+ await page.push('/with-defer-loader');
+ await page.waitForNetworkIdle();
+ const data = (await page.$$text('#itemId'))[0];
+
+ expect(data).toEqual('1233');
+ });
+
afterAll(async () => {
await browser.close();
});
diff --git a/website/docs/guide/basic/data-loader.md b/website/docs/guide/basic/data-loader.md
index 5c0602178..55d688e23 100644
--- a/website/docs/guide/basic/data-loader.md
+++ b/website/docs/guide/basic/data-loader.md
@@ -71,6 +71,51 @@ export const dataLoader = defineDataLoader(async () => {
受小程序环境限制,通过 `dataLoader` 定义的应用级数据加载将在 `App` 的 `onLaunch` 生命周期中进行,页面级数据加载则会在 `Page` 的 `onLoad` 生命周期中,二者均会阻塞页面的 UI 渲染。如果这不是你想要的效果,请按照常规方式进行数据请求。(比如在组件首次 `useEffect` 时发起数据请求)
:::
+
+## 异步消费数据
+
+默认情况下,页面会等待数据请求完成后,再开始渲染,在数据接口比较快的情况下,这可以避免页面的二次渲染。
+
+如果数据接口较慢,也可以选择先渲染不依赖于动态数据的部分,待数据回来后,再重新渲染依赖数据的页面内容。
+
+具体做法如下:
+- 1. 在定义 dataLoader 时标记 defer: true
+- 2. 在消费数据时,使用 Await 组件包裹依赖于数据的页面内容
+
+```tsx title="src/pages/index.tsx"
+import { useData, defineDataLoader, Await } from 'ice';
+
+// 页面组件的 UI 实现
+export default function Home() {
+ const data = useData();
+
+ return (
+ <>
+ Hello ICE
+ loading...} errorElement={Error!
} />
+ {
+ (data) => {JSON.stringify(data)}
+ }
+
+ >
+ );
+};
+
+// 在定义 dataLoader 时标记 defer: true
+export const dataLoader = defineDataLoader(async () => {
+ const data = await fetch('https://example.com/api/xxx');
+ return data;
+}, { defer: true });
+```
+
+注意:
+- 当 dataLoader 被声明为异步时,useData 返回的内容不可直接消费,需由 Await 组件处理
+
+Await 组件接收三个参数
+* resolve 数据请求对象
+* fallback 数据加载过程中展示的 UI
+* errorElement 请求失败时展示的 UI
+
## 静态 dataLoader
当开发者希望通过统一的发送函数处理静态配置以完成 `dataLoader` 时,可以通过自定义 `fetcher` 以完成发送逻辑的统一封装,在 `dataLoader` 中只需要传递一份配置即可。
@@ -186,7 +231,7 @@ export default function Home(props) {
import { useData, defineDataLoader } from 'ice';
export default function Home() {
- const [useInfo, itemInfo] = useData();
+ const [userInfo, itemInfo] = useData();
return (
<>
@@ -196,6 +241,40 @@ export default function Home() {
);
};
+export const dataLoader = defineDataLoader([
+ async () => {
+ const userInfo = await fetch('https://example.com/api/userInfo');
+ return userInfo;
+ },
+ async (ctx) => {
+ const itemInfo = await fetch(`https://example.com/api/itemInfo${ctx?.query?.itemId}`);
+ return itemInfo;
+ },
+]);
+```
+
+多个数据请求的情况下,`useData` 获取的数据也对应的为数组,数组元素和 `dataLoader` 中定义的数据请求的返回值一一对应。
+
+如果 dataLoader 被声明为异步,消费时可以分别 Await 不同的数据,这样可以做到先返回的数据,先渲染。
+
+```tsx
+import { useData, defineDataLoader } from 'ice';
+
+export default function Home() {
+ const [userInfo, itemInfo] = useData();
+
+ return (
+ <>
+
+ { (data) => Hello {data?.name}
}
+
+
+ { (data) => {JSON.stringify(data)}
}
+
+
+ );
+};
+
export const dataLoader = defineDataLoader([
async () => {
const useInfo = await fetch('https://example.com/api/userInfo');
@@ -205,7 +284,5 @@ export const dataLoader = defineDataLoader([
const itemInfo = await fetch(`https://example.com/api/itemInfo${ctx?.query?.itemId}`);
return itemInfo;
},
-]);
-```
-
-多个数据请求的情况下,`useData` 获取的数据也对应的为数组,数组元素和 `dataLoader` 中定义的数据请求的返回值一一对应。
+], { defer: true });
+```
\ No newline at end of file