Skip to content

Commit

Permalink
fix: 修复 forwardRef 组件的错误无法捕获
Browse files Browse the repository at this point in the history
  • Loading branch information
rainke committed Oct 11, 2023
1 parent 4684d31 commit 04cbf01
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 51 deletions.
56 changes: 56 additions & 0 deletions packages/renderer-core/src/hoc/error-boundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { cloneEnumerableProperty } from '@alilc/lowcode-utils';
import adapter from '../adapter';

/**
* 为什么要用 cache,
* 因为 __getHOCWrappedComponent 会返回一个新的组件,
* 如果不缓存,会导致组件每次渲染都是一次新的 mount
*/
const componentCache = new Map();

export function errorBoundaryWrapper(Comp: any, { schema, baseRenderer }) {
const { createElement, Component, forwardRef } = adapter.getRuntime();
const { engine } = baseRenderer.context;
const componentCacheId = schema.id;

if (componentCache.has(componentCacheId)) {
return componentCache.get(componentCacheId);
}

class Wrapper extends Component {
state = { error: null };
componentError = false;
componentDidCatch(error: Error) {
this.componentError = true;
this.setState({ error });
}

render() {
if (this.componentError) {
this.componentError = false;
return engine.createElement(engine.getFaultComponent(), {
...this.props,
error: this.state.error,
componentName: schema.componentName,
});
}
return createElement(Comp, {
...this.props,
ref: this.props.forwardRef,
});
}
}

(Wrapper as any).displayName = Comp.displayName;

let ErrorBoundary = cloneEnumerableProperty(
forwardRef((props: any, ref: any) => {
return createElement(Wrapper, { ...props, forwardRef: ref });
}),
Comp,
);

componentCache.set(componentCacheId, ErrorBoundary);

return ErrorBoundary;
}
5 changes: 3 additions & 2 deletions packages/renderer-core/src/renderer/base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
import { IBaseRendererProps, INodeInfo, IBaseRenderComponent, IBaseRendererContext, IRendererAppHelper, DataSource } from '../types';
import { compWrapper } from '../hoc';
import { IComponentConstruct, leafWrapper } from '../hoc/leaf';
import { errorBoundaryWrapper } from '../hoc/error-boundary';
import logger from '../utils/logger';
import isUseLoop from '../utils/is-use-loop';

Expand Down Expand Up @@ -705,9 +706,9 @@ export default function baseRendererFactory(): IBaseRenderComponent {
*/
get __componentHOCs(): IComponentConstruct[] {
if (this.__designModeIsDesign) {
return [leafWrapper];
return [errorBoundaryWrapper, leafWrapper];
}
return [];
return [errorBoundaryWrapper];
}

__getSchemaChildrenVirtualDom = (schema: IPublicTypeNodeSchema | undefined, scope: any, Comp: any, condition = true) => {
Expand Down
48 changes: 0 additions & 48 deletions packages/renderer-core/src/renderer/renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,55 +105,7 @@ export default function rendererFactory(): IRenderComponent {
return SetComponent;
}

patchDidCatch(SetComponent: any) {
if (!this.isValidComponent(SetComponent)) {
return;
}
if (SetComponent.patchedCatch) {
return;
}
if (!SetComponent.prototype) {
return;
}
SetComponent.patchedCatch = true;

// Rax 的 getDerivedStateFromError 有 BUG,这里先用 componentDidCatch 来替代
// @see https://github.com/alibaba/rax/issues/2211
const originalDidCatch = SetComponent.prototype.componentDidCatch;
SetComponent.prototype.componentDidCatch = function didCatch(this: any, error: Error, errorInfo: any) {
this.setState({ engineRenderError: true, error });
if (originalDidCatch && typeof originalDidCatch === 'function') {
originalDidCatch.call(this, error, errorInfo);
}
};

const engine = this;
const originRender = SetComponent.prototype.render;
SetComponent.prototype.render = function () {
if (this.state && this.state.engineRenderError) {
this.state.engineRenderError = false;
return engine.createElement(engine.getFaultComponent(), {
...this.props,
error: this.state.error,
componentName: this.props._componentName
});
}
return originRender.call(this);
};
if(!(SetComponent.prototype instanceof PureComponent)) {
const originShouldComponentUpdate = SetComponent.prototype.shouldComponentUpdate;
SetComponent.prototype.shouldComponentUpdate = function (nextProps: IRendererProps, nextState: any) {
if (nextState && nextState.engineRenderError) {
return true;
}
return originShouldComponentUpdate ? originShouldComponentUpdate.call(this, nextProps, nextState) : true;
};
}
}

createElement(SetComponent: any, props: any, children?: any) {
// TODO: enable in runtime mode?
this.patchDidCatch(SetComponent);
return (this.props.customCreateElement || createElement)(SetComponent, props, children);
}

Expand Down
1 change: 0 additions & 1 deletion packages/renderer-core/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,6 @@ export interface IRenderComponent {
componentDidCatch(e: any): Promise<void> | void;
shouldComponentUpdate(nextProps: IRendererProps): boolean;
isValidComponent(SetComponent: any): any;
patchDidCatch(SetComponent: any): void;
createElement(SetComponent: any, props: any, children?: any): any;
getNotFoundComponent(): any;
getFaultComponent(): any;
Expand Down

0 comments on commit 04cbf01

Please sign in to comment.