Skip to content

Commit 53b3802

Browse files
committedJan 18, 2021
feat: add ComponentsProvider, LazyComponent
1 parent 1d71888 commit 53b3802

File tree

5 files changed

+371
-4
lines changed

5 files changed

+371
-4
lines changed
 

Diff for: ‎src/components/ComponentsProvider/index.tsx

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import React from 'react';
2+
import { lazyComponent, LazyComponentResult } from '../../utils/lazyComponent';
3+
// eslint-disable-next-line import/no-cycle
4+
import { LazyComponent } from '../LazyComponent';
5+
6+
// Interface
7+
interface ComponentInterfaceWithComponent {
8+
component: React.ComponentType<any>;
9+
}
10+
11+
interface ComponentInterfaceWithUrl {
12+
url: string;
13+
name: string;
14+
}
15+
16+
interface ComponentInterfaceWithDecorator {
17+
decorator: string;
18+
data: any;
19+
}
20+
21+
type ComponentInterface =
22+
| ComponentInterfaceWithComponent
23+
| ComponentInterfaceWithUrl
24+
| ComponentInterfaceWithDecorator;
25+
26+
interface CallbackParamsWithAsync {
27+
async: boolean;
28+
component?: any;
29+
}
30+
31+
interface CallbackParamsWithError {
32+
errCode: string;
33+
}
34+
35+
interface GetComponentCallback {
36+
(e: CallbackParamsWithAsync | CallbackParamsWithError): void;
37+
}
38+
39+
interface ComponentsContextInterface {
40+
addComponent: (code: string, value: ComponentInterface) => void;
41+
getComponent: (
42+
code: string,
43+
callback?: GetComponentCallback,
44+
) => React.ComponentType<any> | LazyComponentResult | null;
45+
}
46+
47+
export interface ComponentsMap {
48+
[k: string]: ComponentInterface;
49+
}
50+
51+
export interface ComponentsProviderProps {
52+
components: ComponentsMap;
53+
}
54+
55+
// Context
56+
const ComponentsContext = React.createContext<ComponentsContextInterface>({
57+
addComponent: () => {},
58+
getComponent: () => null,
59+
});
60+
61+
// Provider
62+
export const ComponentsProvider: React.FC<ComponentsProviderProps> = (props) => {
63+
const { components, children } = props;
64+
const componentsRef = React.useRef(components || {});
65+
66+
function addComponent(code: string, val: ComponentInterface) {
67+
componentsRef.current[code] = val;
68+
}
69+
70+
function getComponent(code: string, callback: GetComponentCallback = () => {}) {
71+
const comp = componentsRef.current[code];
72+
73+
// no component
74+
if (!comp) {
75+
callback({ errCode: 'NO_CODE' });
76+
return null;
77+
}
78+
79+
//
80+
if ('component' in comp) {
81+
callback({ async: false, component: comp.component });
82+
return comp.component;
83+
}
84+
85+
if ('decorator' in comp) {
86+
const component = (compProps: any) => (
87+
<LazyComponent code={comp.decorator} decoratorData={comp.data} {...compProps} />
88+
);
89+
90+
componentsRef.current[code] = { component };
91+
return component;
92+
}
93+
94+
if ('url' in comp) {
95+
const component = lazyComponent(
96+
comp.url,
97+
comp.name,
98+
() => {
99+
componentsRef.current[code] = { component };
100+
callback({ async: true, component });
101+
},
102+
() => {
103+
callback({ errCode: 'ERR_IMPORT_SCRIPT' });
104+
},
105+
);
106+
107+
return component;
108+
}
109+
110+
callback({ errCode: 'NO_HANDLER' });
111+
return null;
112+
}
113+
114+
return (
115+
<ComponentsContext.Provider value={{ addComponent, getComponent }}>
116+
{children}
117+
</ComponentsContext.Provider>
118+
);
119+
};
120+
121+
// Hooks
122+
export function useComponents() {
123+
return React.useContext(ComponentsContext);
124+
}

Diff for: ‎src/components/ErrorBoundary/index.tsx

+7-4
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ export class ErrorBoundary extends React.Component<
1717
React.PropsWithRef<React.PropsWithChildren<ErrorBoundaryProps>>,
1818
ErrorBoundaryState
1919
> {
20-
state = {
21-
error: null,
22-
errorInfo: null,
23-
};
20+
constructor(props: ErrorBoundaryProps) {
21+
super(props);
22+
this.state = {
23+
error: null,
24+
errorInfo: null,
25+
};
26+
}
2427

2528
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
2629
const { onError } = this.props;

Diff for: ‎src/components/LazyComponent/index.tsx

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React, { Suspense } from 'react';
2+
import { ErrorBoundary } from '../..';
3+
// eslint-disable-next-line import/no-cycle
4+
import { useComponents } from '../ComponentsProvider';
5+
6+
interface LazyComponentBaseProps {
7+
fallback?: NonNullable<React.ReactNode> | null;
8+
onError?: (error: Error, info?: React.ErrorInfo) => void;
9+
[k: string]: any;
10+
}
11+
12+
interface LazyComponentPropsWithComponent extends LazyComponentBaseProps {
13+
component: React.ComponentType;
14+
}
15+
16+
interface LazyComponentPropsWithCode extends LazyComponentBaseProps {
17+
code: string;
18+
onLoad?: (e: { async: boolean }) => void;
19+
}
20+
21+
export type LazyComponentProps = LazyComponentPropsWithComponent | LazyComponentPropsWithCode;
22+
23+
export const LazyComponent: React.FC<LazyComponentProps> = (props) => {
24+
const { getComponent } = useComponents();
25+
const { component, code, fallback, onLoad, onError, ...rest } = props;
26+
27+
const Comp =
28+
component ||
29+
getComponent(code, (res) => {
30+
if ('async' in res && onLoad) {
31+
onLoad(res);
32+
} else if ('errCode' in res && onError) {
33+
onError(new Error(res.errCode));
34+
}
35+
});
36+
37+
if (component && onLoad) {
38+
onLoad({ async: false, component });
39+
}
40+
41+
return Comp ? (
42+
<ErrorBoundary onError={onError}>
43+
<Suspense fallback={fallback || null}>
44+
<Comp {...rest} />
45+
</Suspense>
46+
</ErrorBoundary>
47+
) : null;
48+
};
49+
50+
export default LazyComponent;

Diff for: ‎src/index.ts

+9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ export { default as clsx } from 'clsx';
55
export { importScript } from './utils/importScript';
66
export { lazyComponent } from './utils/lazyComponent';
77
export { mountComponent } from './utils/mountComponent';
8+
9+
/* eslint-disable import/no-cycle */
10+
export { ComponentsProvider, useComponents } from './components/ComponentsProvider';
11+
export type { ComponentsProviderProps, ComponentsMap } from './components/ComponentsProvider';
12+
13+
export { LazyComponent } from './components/LazyComponent';
14+
export type { LazyComponentProps } from './components/LazyComponent';
15+
/* eslint-enable import/no-cycle */
16+
817
export { Avatar } from './components/Avatar';
918
export type { AvatarProps, AvatarSize, AvatarShape } from './components/Avatar';
1019
export { Backdrop } from './components/Backdrop';

Diff for: ‎storybook/stories/ComponentProvider.stories.tsx

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import React from 'react';
2+
import { Meta } from '@storybook/react/types-6-0';
3+
4+
import * as ChatUI from '../../src';
5+
import { ComponentsMap } from '../../src';
6+
7+
const { ComponentsProvider, useComponents, LazyComponent } = ChatUI;
8+
9+
export default {
10+
title: 'ComponentsProvider',
11+
component: ComponentsProvider,
12+
} as Meta;
13+
14+
// for Alime Component
15+
declare global {
16+
interface Window {
17+
ChatUI: any;
18+
}
19+
}
20+
21+
window.React = React;
22+
window.ChatUI = ChatUI;
23+
24+
const ctx = {
25+
ui: {
26+
scrollToEnd: () => {},
27+
},
28+
log: {
29+
expo: () => {},
30+
},
31+
};
32+
33+
// components
34+
const components: ComponentsMap = {
35+
slot: {
36+
name: 'AlimeComponentSlot',
37+
url: '//g.alicdn.com/alime-components/slot/0.1.3/index.js',
38+
},
39+
'error-url': {
40+
name: 'AlimeComponentPromptAccess',
41+
url: '//g.alicdn.com/alime-components/slot/0.1.3/no-index.js',
42+
},
43+
'local-component': {
44+
component: ({ data }: { data: string }) => <p>local component: {data}</p>,
45+
},
46+
'my-decorator': {
47+
component: (props) => (
48+
<div>
49+
<h2>`my-decorator`</h2>
50+
<p>props:</p>
51+
<pre>{JSON.stringify(props, null, 4)}</pre>
52+
</div>
53+
),
54+
},
55+
'test-decorator': {
56+
decorator: 'my-decorator',
57+
data: {
58+
jsonUrl: 'this is a url',
59+
d1: 'd1',
60+
d2: 'd2',
61+
},
62+
},
63+
};
64+
65+
function TestLocalComponent() {
66+
return (
67+
<div>
68+
<h1>Example:</h1>
69+
<LazyComponent code="local-component" onLoad={(a: any) => console.log('onLoad:', a)} />
70+
</div>
71+
);
72+
}
73+
74+
export const LocalComponent = () => (
75+
<ComponentsProvider components={components}>
76+
<TestLocalComponent />
77+
</ComponentsProvider>
78+
);
79+
80+
function TestAsyncComponent() {
81+
return (
82+
<div>
83+
<h1>Example:</h1>
84+
<LazyComponent
85+
code="slot"
86+
data={{ list: [{ title: 'item-1' }, { title: 'item-2' }] }}
87+
meta={{}}
88+
ctx={ctx}
89+
onLoad={(a: any) => console.log('onLoad:', a)}
90+
/>
91+
</div>
92+
);
93+
}
94+
95+
export const AsyncComponent = () => (
96+
<ComponentsProvider components={components}>
97+
<TestAsyncComponent />
98+
</ComponentsProvider>
99+
);
100+
101+
function TestNotFoundCode() {
102+
const [errMsg, setErrMsg] = React.useState('');
103+
return (
104+
<div>
105+
<h1>Example:</h1>
106+
<LazyComponent
107+
code="no-code"
108+
onError={(err) => {
109+
setErrMsg(err.message);
110+
}}
111+
/>
112+
{errMsg && <pre>Error message: {errMsg}</pre>}
113+
</div>
114+
);
115+
}
116+
117+
export const NotFoundCode = () => (
118+
<ComponentsProvider components={components}>
119+
<TestNotFoundCode />
120+
</ComponentsProvider>
121+
);
122+
123+
function TestComponentHasError() {
124+
const [errMsg, setErrMsg] = React.useState('');
125+
return (
126+
<div>
127+
<h1>Example:</h1>
128+
<LazyComponent
129+
code="slot"
130+
onError={(err, errInfo) => {
131+
setErrMsg(err.message);
132+
console.log(errInfo);
133+
}}
134+
/>
135+
{errMsg && <pre>Error message: {errMsg}</pre>}
136+
</div>
137+
);
138+
}
139+
140+
export const ComponentHasError = () => (
141+
<ComponentsProvider components={components}>
142+
<TestComponentHasError />
143+
</ComponentsProvider>
144+
);
145+
146+
function TestErrorUrl() {
147+
const [errMsg, setErrMsg] = React.useState('');
148+
return (
149+
<div>
150+
<h1>Example:</h1>
151+
<LazyComponent
152+
code="error-url"
153+
onError={(err) => {
154+
setErrMsg(err.message);
155+
}}
156+
/>
157+
{errMsg && <pre>Error message: {errMsg}</pre>}
158+
</div>
159+
);
160+
}
161+
162+
export const ErrorUrl = () => (
163+
<ComponentsProvider components={components}>
164+
<TestErrorUrl />
165+
</ComponentsProvider>
166+
);
167+
168+
function TestDecorator() {
169+
return (
170+
<div>
171+
<h1>Example:</h1>
172+
<LazyComponent code="test-decorator" data1="foo" data2="bar" />
173+
</div>
174+
);
175+
}
176+
177+
export const Decorator = () => (
178+
<ComponentsProvider components={components}>
179+
<TestDecorator />
180+
</ComponentsProvider>
181+
);

0 commit comments

Comments
 (0)
Please sign in to comment.