Skip to content

Commit

Permalink
fix: [LazyComponent] add onLoad event to decorator, and resolve cir…
Browse files Browse the repository at this point in the history
…cular dependencies
  • Loading branch information
akai committed Mar 8, 2021
1 parent e42a2b8 commit e7d76d1
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 113 deletions.
7 changes: 7 additions & 0 deletions src/components/ComponentsProvider/ComponentsContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from 'react';
import { ComponentsContextInterface } from './interface';

export const ComponentsContext = React.createContext<ComponentsContextInterface>({
addComponent: () => {},
getComponent: () => null,
});
98 changes: 26 additions & 72 deletions src/components/ComponentsProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,17 @@
import React from 'react';
import { lazyComponent, LazyComponentResult } from '../../utils/lazyComponent';
// eslint-disable-next-line import/no-cycle
import { LazyComponent } from '../LazyComponent';
import { lazyComponent } from '../../utils/lazyComponent';
import { LazyComponentWithCode } from '../LazyComponent';
import { ComponentsContext } from './ComponentsContext';
import {
ComponentInterface,
GetComponentCallback,
ComponentsProviderProps,
ComponentsMap,
} from './interface';

export { useComponents } from './useComponents';
export type { ComponentsProviderProps, ComponentsMap };

// Interface
interface ComponentInterfaceWithComponent {
component: React.ComponentType<any>;
}

interface ComponentInterfaceWithUrl {
url: string;
name: string;
}

interface ComponentInterfaceWithDecorator {
decorator: string;
data: any;
}

type ComponentInterface =
| ComponentInterfaceWithComponent
| ComponentInterfaceWithUrl
| ComponentInterfaceWithDecorator;

interface CallbackParamsWithAsync {
async: boolean;
component?: any;
}

interface CallbackParamsWithError {
errCode: string;
}

interface GetComponentCallback {
(e: CallbackParamsWithAsync | CallbackParamsWithError): void;
}

interface ComponentsContextInterface {
addComponent: (code: string, value: ComponentInterface) => void;
getComponent: (
code: string,
callback?: GetComponentCallback,
) => React.ComponentType<any> | LazyComponentResult | null;
}

export interface ComponentsMap {
[k: string]: ComponentInterface;
}

export interface ComponentsProviderProps {
components: ComponentsMap;
}

// Context
const ComponentsContext = React.createContext<ComponentsContextInterface>({
addComponent: () => {},
getComponent: () => null,
});

// Provider
export const ComponentsProvider: React.FC<ComponentsProviderProps> = (props) => {
const { components, children } = props;
const componentsRef = React.useRef(components || {});
Expand All @@ -72,22 +25,28 @@ export const ComponentsProvider: React.FC<ComponentsProviderProps> = (props) =>

// no component
if (!comp) {
callback({ errCode: 'NO_CODE' });
callback({ code, errCode: 'NO_CODE' });
return null;
}

//
if ('component' in comp) {
callback({ async: false, component: comp.component });
if (comp.type !== 'decorator') {
callback({ code, async: false, component: comp.component });
}
return comp.component;
}

if ('decorator' in comp) {
const component = (compProps: any) => (
<LazyComponent code={comp.decorator} decoratorData={comp.data} {...compProps} />
<LazyComponentWithCode
code={comp.decorator}
decoratorData={comp.data}
onLoad={callback}
{...compProps}
/>
);

componentsRef.current[code] = { component };
componentsRef.current[code] = { component, type: 'decorator' };
return component;
}

Expand All @@ -97,17 +56,17 @@ export const ComponentsProvider: React.FC<ComponentsProviderProps> = (props) =>
comp.name,
() => {
componentsRef.current[code] = { component };
callback({ async: true, component });
callback({ code, async: true, component });
},
() => {
callback({ errCode: 'ERR_IMPORT_SCRIPT' });
callback({ code, errCode: 'ERR_IMPORT_SCRIPT' });
},
);

return component;
}

callback({ errCode: 'NO_HANDLER' });
callback({ code, errCode: 'NO_HANDLER' });
return null;
}

Expand All @@ -117,8 +76,3 @@ export const ComponentsProvider: React.FC<ComponentsProviderProps> = (props) =>
</ComponentsContext.Provider>
);
};

// Hooks
export function useComponents() {
return React.useContext(ComponentsContext);
}
53 changes: 53 additions & 0 deletions src/components/ComponentsProvider/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import { LazyComponentResult } from '../../utils/lazyComponent';

interface ComponentInterfaceWithComponent {
component: React.ComponentType<any>;
type?: 'decorator';
}

interface ComponentInterfaceWithUrl {
url: string;
name: string;
}

interface ComponentInterfaceWithDecorator {
decorator: string;
data: any;
}

export type ComponentInterface =
| ComponentInterfaceWithComponent
| ComponentInterfaceWithUrl
| ComponentInterfaceWithDecorator;

interface CallbackParamsWithAsync {
code: string;
async: boolean;
component: React.ComponentType;
}

interface CallbackParamsWithError {
code: string;
errCode: string;
}

export interface GetComponentCallback {
(e: CallbackParamsWithAsync | CallbackParamsWithError): void;
}

export interface ComponentsContextInterface {
addComponent: (code: string, value: ComponentInterface) => void;
getComponent: (
code: string,
callback?: GetComponentCallback,
) => React.ComponentType<any> | LazyComponentResult | null;
}

export interface ComponentsMap {
[k: string]: ComponentInterface;
}

export interface ComponentsProviderProps {
components: ComponentsMap;
}
6 changes: 6 additions & 0 deletions src/components/ComponentsProvider/useComponents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react';
import { ComponentsContext } from './ComponentsContext';

export function useComponents() {
return React.useContext(ComponentsContext);
}
15 changes: 15 additions & 0 deletions src/components/LazyComponent/SuspenseWrap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React, { Suspense } from 'react';
import { ErrorBoundary } from '../ErrorBoundary';
import { LazyComponentPropsWithComponent } from './interface';

export const SuspenseWrap: React.FC<LazyComponentPropsWithComponent> = (props) => {
const { component: Comp, onError, fallback, ...rest } = props;

return Comp ? (
<ErrorBoundary onError={onError}>
<Suspense fallback={fallback || null}>
<Comp {...rest} />
</Suspense>
</ErrorBoundary>
) : null;
};
64 changes: 25 additions & 39 deletions src/components/LazyComponent/index.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,36 @@
import React, { Suspense } from 'react';
import { ErrorBoundary } from '../..';
// eslint-disable-next-line import/no-cycle
import { useComponents } from '../ComponentsProvider';
import React from 'react';
import { SuspenseWrap } from './SuspenseWrap';
import { LazyComponentProps, LazyComponentPropsWithCode } from './interface';
import { useComponents } from '../ComponentsProvider/useComponents';

interface LazyComponentBaseProps {
fallback?: NonNullable<React.ReactNode> | null;
onError?: (error: Error, info?: React.ErrorInfo) => void;
[k: string]: any;
}
export type { LazyComponentProps };

interface LazyComponentPropsWithComponent extends LazyComponentBaseProps {
component: React.ComponentType;
}
export const LazyComponentWithCode: React.FC<LazyComponentPropsWithCode> = (props) => {
const { code, fallback, onLoad, onError, ...rest } = props;
const { getComponent } = useComponents();

interface LazyComponentPropsWithCode extends LazyComponentBaseProps {
code: string;
onLoad?: (e: { async: boolean }) => void;
}
const Comp = getComponent(code, (res) => {
if ('async' in res && onLoad) {
onLoad(res);
} else if ('errCode' in res && onError) {
onError(new Error(res.errCode));
}
});

export type LazyComponentProps = LazyComponentPropsWithComponent | LazyComponentPropsWithCode;
return <SuspenseWrap component={Comp} onError={onError} fallback={fallback} {...rest} />;
};

export const LazyComponent: React.FC<LazyComponentProps> = (props) => {
const { getComponent } = useComponents();
const { component, code, fallback, onLoad, onError, ...rest } = props;

const Comp =
component ||
getComponent(code, (res) => {
if ('async' in res && onLoad) {
onLoad(res);
} else if ('errCode' in res && onError) {
onError(new Error(res.errCode));
}
});

if (component && onLoad) {
onLoad({ async: false, component });
const { component, code, onLoad, ...rest } = props;

if (component) {
if (onLoad) {
onLoad({ async: false, component });
}
return <SuspenseWrap component={component} {...rest} />;
}

return Comp ? (
<ErrorBoundary onError={onError}>
<Suspense fallback={fallback || null}>
<Comp {...rest} />
</Suspense>
</ErrorBoundary>
) : null;
return <LazyComponentWithCode code={code} onLoad={onLoad} {...rest} />;
};

export default LazyComponent;
16 changes: 16 additions & 0 deletions src/components/LazyComponent/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
interface LazyComponentBaseProps {
fallback?: NonNullable<React.ReactNode> | null;
onError?: (error: Error, info?: React.ErrorInfo) => void;
[k: string]: any;
}

export interface LazyComponentPropsWithComponent extends LazyComponentBaseProps {
component: React.ComponentType | null;
}

export interface LazyComponentPropsWithCode extends LazyComponentBaseProps {
code: string;
onLoad?: (e: { async: boolean; component: React.ComponentType<any> }) => void;
}

export type LazyComponentProps = LazyComponentPropsWithComponent | LazyComponentPropsWithCode;
46 changes: 44 additions & 2 deletions storybook/stories/ComponentProvider.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,25 @@ const components: ComponentsMap = {
d2: 'd2',
},
},
'test-async-decorator': {
decorator: 'slot',
data: {
jsonUrl: 'this is a url',
d1: 'd1',
d2: 'd2',
},
},
};

function TestLocalComponent() {
return (
<div>
<h1>Example:</h1>
<LazyComponent code="local-component" onLoad={(a: any) => console.log('onLoad:', a)} />
<LazyComponent
code="local-component"
data="11"
onLoad={(a: any) => console.log('onLoad:', a)}
/>
</div>
);
}
Expand Down Expand Up @@ -169,7 +181,14 @@ function TestDecorator() {
return (
<div>
<h1>Example:</h1>
<LazyComponent code="test-decorator" data1="foo" data2="bar" />
<LazyComponent
code="test-decorator"
data1="foo"
data2="bar"
onLoad={(r) => {
console.log('decorator onLoad', r);
}}
/>
</div>
);
}
Expand All @@ -179,3 +198,26 @@ export const Decorator = () => (
<TestDecorator />
</ComponentsProvider>
);

function TestAsyncDecorator() {
return (
<div>
<h1>Example:</h1>
<LazyComponent
code="test-async-decorator"
data={{ list: [{ title: 'item-1' }, { title: 'item-2' }] }}
meta={{}}
ctx={ctx}
onLoad={(r) => {
console.log('async decorator onLoad', r);
}}
/>
</div>
);
}

export const AsyncDecorator = () => (
<ComponentsProvider components={components}>
<TestAsyncDecorator />
</ComponentsProvider>
);

0 comments on commit e7d76d1

Please sign in to comment.