React / JSX 动态组件名称

323
我正在尝试根据组件类型动态渲染组件。 例如:
var type = "Example";
var ComponentName = type + "Component";
return <ComponentName />; 
// Returns <examplecomponent />  instead of <ExampleComponent />

我尝试了这里提出的解决方案React/JSX动态组件名称

当我编译时(使用gulp的browserify),它给了我一个错误。 它期望XML,而我使用了数组语法。

我可以通过为每个组件创建一个方法来解决这个问题:

newExampleComponent() {
    return <ExampleComponent />;
}

newComponent(type) {
    return this["new" + type + "Component"]();
}

不过这意味着我需要为每个组件创建一个新的方法。这个问题必须有一个更加优雅的解决方案。

我非常乐意听取建议。

编辑:gmfvpereira所指出的,现在官方文档中已经有针对此问题的说明: https://reactjs.org/docs/jsx-in-depth.html#choosing-the-type-at-runtime

16个回答

286

<MyComponent /> 编译成 React.createElement(MyComponent, {}),第一个参数需要是字符串(HTML 标签)或函数(ReactClass)。

你可以将组件类存储在以大写字母开头的变量名中。参见HTML 标签与 React 组件

var MyComponent = Components[type + "Component"];
return <MyComponent />;

编译为

var MyComponent = Components[type + "Component"];
return React.createElement(MyComponent, {});

8
未来的读者可能也会发现{...this.props}很有用,它可以把父组件的props透明地传递给子组件。例如:return <MyComponent {...this.props} /> - Dr.Strangelove
8
请确保将动态变量名首字母大写。 - saada
51
请记住,你的变量应该保存组件本身,而不仅仅是组件的名称作为字符串。 - totymedli
4
如果你也想知道为什么 var 需要大写,可以参考这个链接:https://facebook.github.io/react/docs/jsx-in-depth.html#user-defined-components-must-be-capitalized - Aurelio
8
组件未定义。 - ness-EE
2
对于那些困惑于“Components未定义”的人,请阅读此处的“在运行时选择类型”部分:https://reactjs.org/docs/jsx-in-depth.html#html-tags-vs.-react-components - Flemin Adambukulam

253

这里有一份官方文档,介绍了如何处理这种情况:https://facebook.github.io/react/docs/jsx-in-depth.html#choosing-the-type-at-runtime

基本上文档内容如下:

错误的写法:

import React from 'react';
import { PhotoStory, VideoStory } from './stories';

const components = {
    photo: PhotoStory,
    video: VideoStory
};

function Story(props) {
    // Wrong! JSX type can't be an expression.
    return <components[props.storyType] story={props.story} />;
}

正确:

import React from 'react';
import { PhotoStory, VideoStory } from './stories';

const components = {
    photo: PhotoStory,
    video: VideoStory
};

function Story(props) {
    // Correct! JSX type can be a capitalized variable.
    const SpecificStory = components[props.storyType];
    return <SpecificStory story={props.story} />;
}

75
非常重要的事情:一个大写字母开头的变量。 - mpyw
5
谢谢你的出色回答。对于以下读者,请注意,map对象内部的值(此处为const components和值PhotoStory和VideoStory)必须没有引号-否则,组件将无法渲染,您会收到错误提示-就像我遇到的情况一样,我浪费了时间...... - Erez Lieberman
这真的应该成为被接受的答案。它更清晰,有更好的例子。 - Patrick Yan
当我使用这个时,它会给出以下警告:警告:函数不能作为React子元素。如果您从render返回组件而不是<Component />,则可能会发生这种情况。或者您可能意味着调用此函数而不是返回它。 - Mees

38

应该有一个容器,将组件名称映射到所有应该动态使用的组件中。组件类应该在容器中注册,因为在模块化环境中,否则没有单一的地方可以访问它们。组件类不能仅通过其名称进行识别,而需要明确指定它们,因为生产中的函数name被缩小了。

组件映射

它可以是普通对象:

class Foo extends React.Component { ... }
...
const componentsMap = { Foo, Bar };
...
const componentName = 'Fo' + 'o';
const DynamicComponent = componentsMap[componentName];
<DynamicComponent/>;

或者 Map 实例:

const componentsMap = new Map([[Foo, Foo], [Bar, Bar]]);
...
const DynamicComponent = componentsMap.get(componentName);

普通对象更适合,因为它受益于属性速记。

桶模块

使用命名导出的桶模块可以充当这样的映射:

// Foo.js
export class Foo extends React.Component { ... }

// dynamic-components.js
export * from './Foo';
export * from './Bar';

// some module that uses dynamic component
import * as componentsMap from './dynamic-components';

const componentName = 'Fo' + 'o';
const DynamicComponent = componentsMap[componentName];
<DynamicComponent/>;

使用每个模块一个类的代码风格,这很有效。

装饰器

可以将装饰器用于类组件以获得语法糖,但仍需要显式指定类名并在映射中注册:

const componentsMap = {};

function dynamic(Component) {
  if (!Component.displayName)
    throw new Error('no name');

  componentsMap[Component.displayName] = Component;

  return Component;
}

...

@dynamic
class Foo extends React.Component {
  static displayName = 'Foo'
  ...
}

装饰器可以作为高阶组件与函数组件一起使用:

const Bar = props => ...;
Bar.displayName = 'Bar';

export default dynamic(Bar);

使用非标准的 displayName 而不是随机属性,还可以更好地进行调试。


20

通过引入React.lazy,我们现在可以使用真正的动态方法来导入组件并渲染它。

import React, { lazy, Suspense } from 'react';

const App = ({ componentName, ...props }) => {
  const DynamicComponent = lazy(() => import(`./${componentName}`));
    
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <DynamicComponent {...props} />
    </Suspense>
  );
};

当然,这种方法对文件层次结构作出了某些假设,可能会使代码容易破坏。


你的返回语句应该返回<DynamicComponent />吧?看起来你在第4行编辑了组件名称,但是第8行没有修改。 - jymbob

15

在所有具有组件映射选项的情况下,我还没有找到使用ES6短语法定义地图的最简单方法:

import React from 'react'
import { PhotoStory, VideoStory } from './stories'

const components = {
    PhotoStory,
    VideoStory,
}

function Story(props) {
    //given that props.story contains 'PhotoStory' or 'VideoStory'
    const SpecificStory = components[props.story]
    return <SpecificStory/>
}

12

我想出了一个新的解决方案。请注意,我正在使用ES6模块,因此我需要引用该类。您也可以定义一个新的React类。

var components = {
    example: React.createFactory( require('./ExampleComponent') )
};

var type = "example";

newComponent() {
    return components[type]({ attribute: "value" });
}

1
@klinore 你尝试访问 default 属性了吗?例如:require('./ExampleComponent').default? - Khanh Hua

11

对于一个包装组件,一个简单的解决方案就是直接使用React.createElement(使用ES6)。

import RaisedButton from 'mui/RaisedButton'
import FlatButton from 'mui/FlatButton'
import IconButton from 'mui/IconButton'

class Button extends React.Component {
  render() {
    const { type, ...props } = this.props

    let button = null
    switch (type) {
      case 'flat': button = FlatButton
      break
      case 'icon': button = IconButton
      break
      default: button = RaisedButton
      break
    }

    return (
      React.createElement(button, { ...props, disableTouchRipple: true, disableFocusRipple: true })
    )
  }
}

8

If your components are global you can simply do:

var nameOfComponent = "SomeComponent";
React.createElement(window[nameOfComponent], {});


1
这个非常好用,特别是在使用Rails的时候。接受的答案对我不起作用,因为“Components”数组未定义。 - Vadim
4
不要将任意命名的对象附加到全局作用域(euw),你可以考虑维护一个组件注册表,当需要时注册并检索组件引用。 - slashwhatever
window[nameOfComponent] is unnecessary here and can be replaced by nameOfComponent - Nate-Wilkins
为什么在React中提到全局变量window时,window['name']会变成未定义? - Nuha Dzikri

5

在大量组件的情况下,拥有一个地图看起来一点也不好。我真的很惊讶没有人建议这样做:

var componentName = "StringThatContainsComponentName";
const importedComponentModule = require("path/to/component/" + componentName).default;
return React.createElement(importedComponentModule); 

当我需要以JSON数组的形式加载大量组件时,这个真的对我有帮助。


这与我的工作接近,并引导我朝着正确的方向前进。直接尝试调用React.createElement(MyComponent)会抛出错误。具体来说,我不希望父组件必须知道所有要导入的组件(在映射中)-因为那似乎是一个额外的步骤。相反,我使用了const MyComponent = require("path/to/component/" + "ComponentNameString").default; return <MyComponent /> - semaj1919
这个。它的一个功能。CreateElement。我不知道其他帖子在说什么。 - undefined

3
假设我们有一个“flag”,它与“state”或“props”没有区别:
import ComponentOne from './ComponentOne';
import ComponentTwo from './ComponentTwo';

~~~

const Compo = flag ? ComponentOne : ComponentTwo;

~~~

<Compo someProp={someValue} />

使用标志Compo填充其中一个ComponentOneComponentTwo,然后Compo可以像React组件一样运行。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接