Nazwa komponentu dynamicznego React / JSX

168

Staram się dynamicznie renderować komponenty na podstawie ich typu.

Na przykład:

var type = "Example";
var ComponentName = type + "Component";
return <ComponentName />; 
// Returns <examplecomponent />  instead of <ExampleComponent />

Wypróbowałem rozwiązanie zaproponowane tutaj Dynamiczne nazwy komponentów React / JSX

To dało mi błąd podczas kompilacji (używając browserify dla gulp). Oczekiwano XML, w którym użyłem składni tablicowej.

Mogę rozwiązać ten problem, tworząc metodę dla każdego komponentu:

newExampleComponent() {
    return <ExampleComponent />;
}

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

Ale to oznaczałoby nową metodę dla każdego komponentu, który tworzę. Musi istnieć bardziej eleganckie rozwiązanie tego problemu.

Jestem bardzo otwarta na sugestie.

Sam
źródło

Odpowiedzi:

158

<MyComponent />kompiluje się do React.createElement(MyComponent, {}), który oczekuje ciągu znaków (znacznik HTML) lub funkcji (ReactClass) jako pierwszego parametru.

Możesz po prostu przechowywać klasę komponentów w zmiennej o nazwie zaczynającej się od dużej litery. Zobacz tagi HTML a komponenty React .

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

kompiluje się do

var MyComponent = Components[type + "Component"];
return React.createElement(MyComponent, {});
Alexandre Kirszenberg
źródło
5
Przyszli czytelnicy prawdopodobnie również uznają za {...this.props}przydatne do przezroczystego przekazywania rekwizytów do komponentów podrzędnych z rodzica. Na przykładreturn <MyComponent {...this.props} />
dr Strangelove
4
Upewnij się również, że nazwa zmiennej dynamicznej jest pisana wielkimi literami.
saada
28
Pamiętaj, że zmienna powinna zawierać sam komponent , a nie tylko jego nazwę w postaci łańcucha .
totymedli
3
Jeśli również zastanawiasz się, dlaczego zmienna var musi być
pisana
3
Komponenty są niezdefiniowane
ness-EE
144

Oficjalna dokumentacja dotycząca postępowania w takich sytuacjach jest dostępna tutaj: https://facebook.github.io/react/docs/jsx-in-depth.html#choosing-the-type-at-runtime

Zasadniczo mówi:

Źle:

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} />;
}

Poprawny:

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} />;
}
gmfvpereira
źródło
25
BARDZO WAŻNA RZECZ: zmienna
pisana
4
Poza tym, że jest to oficjalna dokumentacja, jest to z pewnością najlepsza odpowiedź i najbardziej ustrukturyzowane rozwiązanie. Może dlatego jest w dokumentacji
:)
1
Dzięki za świetną odpowiedź. Dla następujących czytelników zwróć uwagę, że wartość w obiekcie mapy (obiekt mapy to komponenty const, a wartość to PhotoStory i VideoStory) musi być bez cudzysłowów - w przeciwnym razie komponent nie będzie renderowany i pojawi się błąd - w moim przypadkiem przegapiłem to i po prostu zmarnowałem czas ...
Erez Lieberman
11

Powinien istnieć kontener, który odwzorowuje nazwy komponentów na wszystkie komponenty, które mają być używane dynamicznie. Klasy komponentów powinny być zarejestrowane w kontenerze, ponieważ w środowisku modułowym nie ma innego miejsca, w którym można by uzyskać do nich dostęp. Klas komponentów nie można zidentyfikować za pomocą nazw bez ich jawnego określenia, ponieważ funkcja namejest zminimalizowana w produkcji.

Mapa komponentów

Może to być zwykły przedmiot:

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

Lub na Mapprzykład:

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

Zwykły obiekt jest bardziej odpowiedni, ponieważ korzysta ze skrótu własności.

Moduł beczki

Moduł beczka z wymienionych eksportu może działać jako takiej mapie:

// 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/>;

Działa to dobrze z jedną klasą na styl kodu modułu.

Dekorator

Dekoratorów można używać z komponentami klas dla cukru składniowego, ale nadal wymaga to jawnego określenia nazw klas i zarejestrowania ich w mapie:

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'
  ...
}

Dekorator może być używany jako element wyższego rzędu z elementami funkcjonalnymi:

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

export default dynamic(Bar);

Użycie niestandardowejdisplayName właściwości zamiast losowej również jest korzystne dla debugowania.

Estus Flask
źródło
Dzięki! Ta mapa komponentów jest czysta i ładna :)
Leon Gaban
10

Wymyśliłem nowe rozwiązanie. Zwróć uwagę, że używam modułów ES6, więc wymagam klasy. Zamiast tego możesz także zdefiniować nową klasę React.

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

var type = "example";

newComponent() {
    return components[type]({ attribute: "value" });
}
Sam
źródło
1
@klinore Czy próbowałeś uzyskać dostęp do defaultatrybutu? czyli: require ('./ ExampleComponent'). default?
Khanh Hua
7

Jeśli Twoje komponenty są globalne, możesz po prostu:

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

Promień
źródło
1
Działa to pięknie, zwłaszcza jeśli używasz Railsów. Zaakceptowana odpowiedź nie działa dla mnie, ponieważ Componentstablica nie jest zdefiniowana.
Vadim
3
Zamiast dołączać dowolnie nazwane obiekty do zasięgu globalnego (euw), należy rozważyć prowadzenie rejestru komponentów, który można zarejestrować, a następnie w razie potrzeby pobierać odwołania do komponentów.
slash cokolwiek
6

W przypadku komponentu opakowania prostym rozwiązaniem byłoby użycie React.createElementbezpośrednio (przy użyciu 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 })
    )
  }
}
Ricardo Pedroni
źródło
Właściwie mam komponent o tym samym celu w moim projekcie)
Dziamid
2

We wszystkich opcjach z mapami komponentów nie znalazłem najprostszego sposobu na zdefiniowanie mapy przy użyciu krótkiej składni 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/>
}
Stalinko
źródło
1

Posiadanie mapy wcale nie wygląda dobrze przy dużej ilości komponentów. Właściwie jestem zaskoczony, że nikt nie zasugerował czegoś takiego:

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

Ten naprawdę pomógł mi, gdy musiałem wyrenderować całkiem dużą liczbę komponentów załadowanych w postaci tablicy json.

Arthur Z.
źródło
Jest to bliskie temu, co zadziałało dla mnie i poprowadziło mnie we właściwym kierunku. Próba React.createElement(MyComponent)bezpośredniego wywołania spowodowała błąd. W szczególności nie chcę, aby rodzic musiał znać wszystkie komponenty do zaimportowania (w mapowaniu) - ponieważ wydaje się to dodatkowym krokiem. Zamiast tego użyłem const MyComponent = require("path/to/component/" + "ComponentNameString").default; return <MyComponent />.
semaj1919
0

Podejrzewamy, że chcemy uzyskać dostęp do różnych widoków z dynamicznym ładowaniem komponentów. Poniższy kod przedstawia działający przykład, jak to zrobić, używając ciągu przeanalizowanego z ciągu wyszukiwania adresu URL.

Załóżmy, że chcemy uzyskać dostęp do strony „snozberrys” z dwoma unikalnymi widokami przy użyciu następujących ścieżek URL:

'http://localhost:3000/snozberrys?aComponent'

i

'http://localhost:3000/snozberrys?bComponent'

definiujemy kontroler naszego widoku w ten sposób:

import React, { Component } from 'react';
import ReactDOM from 'react-dom'
import {
  BrowserRouter as Router,
  Route
} from 'react-router-dom'
import AComponent from './AComponent.js';
import CoBComponent sole from './BComponent.js';

const views = {
  aComponent: <AComponent />,
  console: <BComponent />
}

const View = (props) => {
  let name = props.location.search.substr(1);
  let view = views[name];
  if(view == null) throw "View '" + name + "' is undefined";
  return view;
}

class ViewManager extends Component {
  render() {
    return (
      <Router>
        <div>
          <Route path='/' component={View}/>
        </div>
      </Router>
    );
  }
}

export default ViewManager

ReactDOM.render(<ViewManager />, document.getElementById('root'));
1-14x0r
źródło
0

Załóżmy, że mamy a flag, nie różni się od stateor props:

import ComponentOne from './ComponentOne';
import ComponentTwo from './ComponentTwo';

~~~

const Compo = flag ? ComponentOne : ComponentTwo;

~~~

<Compo someProp={someValue} />

Z flagą Compowypełnij jedną z ComponentOnelub, ComponentTwoa następnie Compomoże działać jak komponent reagujący.

AmerllicA
źródło
-1

Użyłem nieco innego podejścia, ponieważ zawsze znamy nasze rzeczywiste komponenty, więc pomyślałem, że zastosuję obudowę przełącznika. Również całkowita liczba komponentów w moim przypadku wynosiła około 7-8.

getSubComponent(name) {
    let customProps = {
       "prop1" :"",
       "prop2":"",
       "prop3":"",
       "prop4":""
    }

    switch (name) {
      case "Component1": return <Component1 {...this.props} {...customProps} />
      case "Component2": return <Component2 {...this.props} {...customProps} />
      case "component3": return <component3 {...this.props} {...customProps} />

    }
  }
abhirathore2006
źródło
Po prostu znowu to natknąłem To jest sposób na zrobienie tego. I tak zawsze znasz swoje komponenty i musisz je załadować. To świetne rozwiązanie. Dzięki.
Jake
-1

Edycja: inne odpowiedzi są lepsze, zobacz komentarze.

W ten sposób rozwiązałem ten sam problem:

...
render : function () {
  var componentToRender = 'component1Name';
  var componentLookup = {
    component1Name : (<Component1 />),
    component2Name : (<Component2 />),
    ...
  };
  return (<div>
    {componentLookup[componentToRender]}
  </div>);
}
...
Hammad Akhwand
źródło
3
Prawdopodobnie nie chcesz tego robić, ponieważ React.createElementzostanie wywołany dla każdego składnika w obiekcie wyszukiwania, nawet jeśli tylko jeden z nich jest renderowany na raz. Co gorsza, umieszczając obiekt odnośnika w rendermetodzie komponentu rodzica, zrobi to ponownie za każdym razem, gdy element nadrzędny jest renderowany. Najlepsze odpowiedzi to znacznie lepszy sposób na osiągnięcie tego samego.
Inkling
2
@Inkling, zgadzam się. To było, kiedy dopiero zaczynałem z Reactem. Napisałem to, a potem zapomniałem o tym, kiedy wiedziałem lepiej. Dzięki za zwrócenie uwagi.
Hammad Akhwand