【问题标题】:React: is it possible to render a component object that was created with "new"?React:是否可以渲染使用“new”创建的组件对象?
【发布时间】:2019-06-23 14:41:26
【问题描述】:

我正在构建一个用户可以动态更改页面布局的应用程序。有两个接口。它们一起提供了可用于构建布局的组件集合:

export interface IComponentProvider {
    key : string;
    displaylabel : string;
    description ?: string;
    render(): React.ReactNode;
}

export interface IComponentCollection {
    forEach(callbackfn: (value: IComponentProvider, key: string) => void, thisArg?: any): void;
    get(key: string): IComponentProvider | undefined;
    has(key: string): boolean;
    readonly size: number;
}

注意事项:

  • IComponentProvider 可以通过 React.Component 实现。它实际上是一个特殊的组件,有一个键和一个显示标签。密钥用于标识集合中的组件。 displaylabel 用于给它一个友好的名称。 (因此用户将看到“用户详细信息”而不是“userDetailPanelId132”)。
  • Map<string, React.Component> 实现 IComponentCollection。实际上,在大多数情况下,我会使用 Map 来实现它。

应该使用两个类来呈现布局。有一个DynamicLayoutConfiguration 类定义了布局的结构。它存储组件类型和组件引用的树。例如:

{
  type: "tabs"
  items: [
     {type:"tab", title:"First tab", 
      items:[
        {
         type:"component", 
         key:"masterComponent1"}
     ]},
     {type:"tab", title:"Second tab", 
      items:[
        {
         type:"component", 
         key:"detailPanel1"}
     ]},
  ]
}

布局配置不是一个组件,它不包含实际的组件。只有它们是这些组件的关键。可以保存/加载配置。这为布局提供了持久性。

最后一部分是DynamicLayout组件,它获取一个集合和一个配置,它应该能够渲染布局:

interface IDynamicLayoutProps {
    configuration: DynamicLayoutConfiguration;
    collection: IComponentCollection;
}


export class DynamicLayout extends React.PureComponent<IDynamicLayoutProps> {
   public render() {
        /* Here we should render, using this.props.configuration and this.props.components */
        return null; // ???
   }
}

上面的示例配置应该呈现如下内容:

<Tabs>
   <TabList>
     <Tab>First tab</Tab>
     <Tab>Second tab</Tab>
   </TabList>
   <TabPanel>{this.props.components.get('masterComponent1').render()}</TabPanel>
   <TabPanel>{this.props.components.get('detailPanel1').render()}</TabPanel>
</Tabs>

为什么我需要这个:因为相同的数据必须以各种方式呈现给用户,并且我希望用户能够定义自己的布局。有一个单独的布局编辑器,用户可以在其中创建带有容器、行、列、选项卡、手风琴等的 UI 组件树结构,并且他们可以将组件插入到此布局中(来自组件集合)。

问题来了。显然,在用户创建布局配置之前需要创建组件集合。例如,如果我有一个名为 DetailPanel 的组件:

export class DetailPanel extends React.Component<...>{...}

那么理论上,我可以这样做:

const components = new Map<string,IComponentProvider>();
const detailPanel = new DetailPanel(detailPanelProps);
components.set(detailPanel.key, detailPanel);

然后为用户打开布局编辑器,他可以在其中使用 DetailPanel(如果他愿意的话)。但请注意,DetailPanel 是使用 new 关键字创建的。

如何渲染以这种方式创建的组件?显然,如果我只是从渲染方法返回该对象,那么我会得到一个“对象作为反应子无效”的错误。我试图返回render()的结果:

<TabPanel>
  {this.props.components.get(componentKey).render()}
</TabPanel>

虽然这会渲染组件,但它不会调用componentDidMountcomponentWillUnmount,通常它是有缺陷的,无法使用。

我认为我不应该使用new DetailPanel() 手动创建这些组件。 但我不知道还能怎么办?如何在不创建组件的情况下创建一般的“组件集合”?如果我必须手动创建它们,那么如何正确渲染它们?

更新:正如@Berouminum 指出的那样,没有好的解决方案。只能使用 JSX 或 React.createElement 渲染元素。这些都不会真正立即创建对象,只有在需要它们时。这意味着要么我使用 React.createElement 创建它们,然后我无法访问 key 和 displaylabel 属性,要么使用 new 关键字(普通对象构造)创建它们,但我将无法渲染正确的。

到目前为止,我发现的最佳解决方法是将 IComponentProvider 更改为具有“createElement”方法而不是渲染方法:

export interface IComponentProvider {
    key : string;
    displaylabel : string;
    description ?: string;
    createElement(): React.ReactElement;
}

任何想要实现这个接口的组件,createElement 方法都可以简单实现:

class ExampleComponent extends React.Component<IProps,IState> impements IComponentProvider {
   public get key() { return this.calculateKey(); }
   public get displaylabel() { return this.calculateDisplaylabel(); }
   public get description() { return this.calculateDescription(); }
   public createElement() {
       return React.createElement(ExampleComponent,this.props, null);
   }
}

有了这个,就可以使用key/displaylabel属性,调用对象上的方法,并正确渲染:

const exampleComponent = new ExampleComponent(props);
/* It is possible to use its properties here. */
components.set(exampleComponent.key, exampleComponent);
/* It is also possible to render it PROPERLY, all lifecycle methods called correctly. */
class SomeContainer extends React.Component<...> {
   ...
   public render() {
       return <div>
                ....
              {exampleComponent.createElement()}
              ...
       </div>;

   }

当然,这个解决方案也是有缺陷的,因为对象会被构造多次(一次手动使用新的关键字,并且可能在需要元素时由 React 多次创建)。所以被渲染的对象实际上是一个不同的实例。但到目前为止,这是我发现的最好的解决方法。它可重复使用且足够通用。

更新 2:如果我尝试将它与 MobX 一起使用,提供的解决方案将完全被破坏。这是因为渲染组件和原始组件是不同的对象。如果我在原始对象上设置一个 observable,那么它不会对渲染对象产生任何影响。有一个非常混乱的解决方法,但可能不值得。

【问题讨论】:

    标签: reactjs


    【解决方案1】:

    在 React 中有两种方法可以实例化一个组件。

    通过 JSX

    const Comp = <Component prop1={prop1} prop2={prop2} />;
    

    通过React.createElement('element', props, children)

    const Comp = React.createElement(Component, { prop1, prop2 }, null);
    

    创建的组件随后可以通过从渲染函数返回来渲染(就像任何其他元素一样)。您不应该尝试手动调用渲染。

    我为你构建了一个小例子。您可以通过这种方式渲染无功能组件和类组件。 ComponentDidMount 也被调用了

    const A = () => <div>A</div>;
    
    class B extends React.Component {
        componentDidMount() {
            console.log('I mounted!');
        }
    
        render() {
            return <div>B</div>
        }
    }
    
    export default class App extends React.Component {
        render() {
            const map = new Map();
            map.set('A', {
                displayLabel: 'Label A',
                description: 'Description A',
                component: A()
            });
            map.set('B1', {
                displayLabel: 'Label B1',
                description: 'Description B1',
                component: <B />
            });
            map.set('B2', {
                displayLabel: 'Label B2',
                description: 'Description B2',
                component: React.createElement(B)
            });
    
            const {
                displayLabel,
                description,
                component
            } = map.get('A');
    
            return (
                <div>
                    <p>{displayLabel}</p>
                    <p>{description}</p>
                    {component}
                </div>
            );
        }
    }
    

    【讨论】:

    • 我又错过了一件事情。假设我将这些元素放入地图中。它将是元素的映射,而不是对象的映射。我将无法访问他们的 displaylabel 和 description 属性或其他任何内容。但我需要访问这些 - 没有它们,我将无法创建布局编辑器(最终用户需要查看显示标签)。如何定义允许访问元素和 displaylabel/description 属性的地图?
    • @nagylzs 你可以用一个包含附加信息的对象来包装你的组件。我已经更新了我的答案。
    • 我希望它们是字符串常量。这些属性被实现为属性获取器。无论如何我明天都会接受你的回答,因为我认为没有更好的解决方案。 :-(
    • 换句话说:当附加信息是对象的属性时,您的示例无法工作。仅仅是因为 React.createElement 不会创建对象。它使所有属性都无法访问,并且无法将它们放入集合中并在布局编辑器中使用它们。
    猜你喜欢
    • 2012-05-30
    • 2018-11-29
    • 1970-01-01
    • 2021-09-01
    • 2015-11-16
    • 2017-09-20
    • 2020-07-12
    • 2019-11-15
    • 1970-01-01
    相关资源
    最近更新 更多