Short version: I have a component type (a class or a function) and props for it. I need to "render" the component to obtain its representation in JSX elements.
(I use the quotes because I mean «render into JSX elements» not «render into UI» and I am not sure about the terminology.)
Example:
const Foo = (props) => <div><Bar>{props.x + props.y}</Bar></div>;
// is an equivalent of `const elements = <div><Bar>3</Bar></div>;`
const elements = render2elements(Foo, { x: 1, y: 2 });
function render2elements(type, props) {
/* what should be here? */
}
Long version (for background story enthusiasts, may be skipped imo)
I have a React code whose very simplified version looks like this:
function Baby(props) {
/* In fact, it does not even matter what the component renders. */
/* It is used primarily as a configuration carrier. */
}
function Mother({ children }) {
const babies = getAllBabies(React.Children.toArray(children));
const data = parseData(babies);
return buildView(data);
}
function SomeOtherComponent(props) {
const { someProps1, someProps2,
someProps3, someCondition } = someLogic(props);
return (
<Mother>
<Baby {...someProps1} />
<Baby {...someProps2} />
{someCondition ? <Baby {...someProps3} /> : null}
</Mother>
);
}
It may be strange but it works. :) Until someone wants to do a little refactoring:
function Stepmother(props) {
const { someProps1, someProps2,
someProps3, someCondition } = someLogic(props);
return (
<>
<Baby {...someProps1} />
<Baby {...someProps2} />
{someCondition ? <Baby {...someProps3} /> : null}
</>
);
}
function SomeOtherComponent(props) {
return <Mother><Stepmother {...props} /></Mother>;
}
Now the Mother receives in its children only a JSX element for the Stepmother and can not parse the JSX elements for the Baby'ies. :(
So we return to my original question: I need to "render" Stepmother and then parse its internal JSX representation. But how can I do this?
P.S. I used functional components for brevity, but of course, all examples could use class components as well.
Thank you.
Don't do that.
I strongly encourage you to just rethink this solution altogether, ESPECIALLY if
It is used primarily as a configuration carrier.
...but.
So this kinda works however there's a couple of caveats:
if a component passed to that function is a class component and has some state, you won't be able to use any of it, in general it will probably cause a ton of issues that I'm not aware of
if a component passed is a function component, you can't use any hooks. It will just throw an error at you.
function render2elements(component, props) {
if (component.prototype.isReactComponent) {
return new component(props).render();
}
return component(props);
}
So if your "babies" are really simple this technically would work. But you just shouldn't refactor it the way you want and, again, ideally rethink this whole concept.
Related
I am currently working on a project that requires dynamically injecting one component into another.
My project is using Redux, so I came up with two possible solutions which both have their advantages and disadvantages, but I don't know which one to choose. I know that by nature, React encourages composition, but I'm still curious to know if the second approach (simpler and faster to use) is still good :
export const SlideOverComponents = {
'UserCreate': UserCreate,
'UserUpdate': UserUpdate,
};
The idea is to register all components that can be injected as a key value pair, and dispatch a Redux action with the key and the props required by this component.
{(!!componentKey && !!SlideOverComponents[componentKey]) && React.createElement(SlideOverComponents[componentKey], props)}
Then in my parent container, I just read this key and use the React.createElement to display the injected one.
This solution is working fine and is easy and fast to use because I just have to register any new component to the object to make it work.
Is this approach "ok" ? Or should I use composition ?
(I'm asking from a "good practice" or "anti-pattern" point of view.)
Yes that's fine, as long as the interface between all of the SlideOverComponents are completely identical. Your code is more verbose than it needs to be. You don't need createElement either if you assign it to a variable first
const Component = SlideOverComponents[componentKey]
return (
<div>
{Component && <Component {...props} />}
</div>
)
Edit:
I noticed that you are using TypeScript from other answers. Considering that, I still think you can use Composition but with types using String Literal Types like this:
type SlideOverComponentsType = "update" | "create";
type SlideOverComponentsProps = UserUpdateProps | UserCreateProps;
type SlideOverProps = {
key: SlideOverComponentsType;
} & SlideOverComponentsProps;
function SlideOver({ key, ...props }: SlideOverProps) {
switch (key) {
case "update":
return <UserUpdate {...props} />;
case "create":
return <UserCreate {...props} />;
default:
return null; // this will never happen but need to be addressed
}
}
And with an approach like that, you don't need an "Object" to store all the possible types of SlideOverComponents. You also guarantee that the props will always be using the proper interface and if eventually, you pass it wrongly TS will warn you about that.
Again: consider using types instead of declaring "options" as objects for cases like this.
Hope that this could help you or give you some good ideas!
Original Answer:
You can still use Composition for this and create some kind of check or `switch` statement inside the "Generic" Component. That way you could avoid adding so many checks(`if`s) outside of the parent component and guarantee that eventually non-existing `keys` could fallback to a default behavior or even to an error.
There are several ways of implementing it but one using switch that I like is this one:
function UserInteraction({ key, ...props }) {
switch (key) {
case "create": {
return <UserCreate {...props} />;
}
case "update": {
return <UserUpdate {...props} />;
}
default: {
return null;
// or you could thrown an error with something like
throw new Error(`Error: key ${key} not present inside component User`);
}
}
}
You could also use the Object.keys() method to accomplish almost the same behavior:
const UserInteractionOptions = {
"create": UserCreate,
"update": UserUpdate,
}
function UserInteraction({ key, ...props }) {
if (!Object.keys(UserInteractionOptions).includes(key)) {
return null;
// or you could thrown an error with something like
throw new Error(`Error: key ${key} not present inside component User`);
}
const InteractionComponent = UserInteractionOptions[key];
return <InteractionComponent {...props} />;
}
The main idea is to isolate the logic from deciding which component to render (and if it can be rendered) inside that component.
For future reading, you could check on TypeScript and how this can be easily handled by types, coercion, and the checks for non-present keys could be made before even the code runs locally.
A little of nitpicking: you are not "injecting" a Component inside another Component. You are just passing a key to deciding if the Parent Component renders or not the Child component through a flag. The injection of one Component into another involves passing the full component as a prop and just rendering it (or customizing it, eventually).
You could look at how React decides to render the children prop and how it decides if it is null, a string, or a ReactComponent to render an actual component. Also, a good topic to research is Dependency Injection.
As a simple example, injecting a component could looks like this:
function Label({ text }) {
return <p>{text}</p>;
}
function Input({ Label, ...props }) {
return (
<div>
<Label />
<input {...props} />
</div>
);
}
I'm trying to dynamically change an element name for reuse of a function.
static renderDetails(props, parentTableElementOpen, parentTableElementClose, ) {
let coverageRows;
if (props.length !== 0) {
return (
<span>
{parentTableElementOpen}
{props.map((columnData, index) => {
if (index === props.length - 1) {
coverageRows = (<TableRowCol classNames={styles.text_align_right}>{columnData}</TableRowCol>);
}
else {
coverageRows = (<TableRowCol>{columnData}</TableRowCol>);
}
return coverageRows;
})}
{parentTableElementClose}
</span>
);
}
return null;
}
The call to this function is below.
Utils.renderDetails(this.props.columnData, '<TableRow>', '</TableRow>');
The parentTableElementOpen and parentTableElementClose will have the names of the elements I'm after.
The rendered page doesn't seem to recognize them and instead of a <TableRow> </TableRow> element type it renders just text <TableRow> </TableRow>
Maybe a bit tricky or overly complicated what I'm trying to do here but thought it could be a good refactor between 2 identical functions.
There might be a solution that actually could work in the way you described but I think you're thinking of this using an HTML mindset. Keep in mind that with React you're rendering a Component which is not an HTML Tag/XML even though it shares similarities with the syntax.
In your case you're passing a string so it is rendering a string.
I think what you want is a generic component that renders the children, not a function that tries to pick a component. Maybe something like this:
class MyTableRow extends React.Component {
render() {
return ( //do whatever customization you want here.
<TableRowCol>
{this.props.children} //renders what's "inside" the tag. You can pass this or specify it in the JSX
</TableRowCol>
)
}
}
If you think about what you're doing in that utility call you're actually specifying the tag you want to use and then just passing props which in the world of React is identical to:
//Some render function
...
<MyUtilityObject props={props} />
...
My first instinct would be to "invert" the design to use components as that seems to be how React is designed.
EDIT
I didn't realize that element.props.children was readonly so the idea below isn't going to work.
My suggestion in this case would be as above. In general this abstract method really isn't doing anything and could be refactored into a custom component so instead of a function call you use the component
<MyTable>
{ row.map( (row, index) => {
switch(row.type) {
case 'data1':
return <MyCustomRow row={row} key={index} />
case 'data2':
return <MyCustomRow2 row={row} key={index} />
default:
return null
}
})
</MyTable>
NOPE
Now that being said, if you wanted to maintain this signature and you have a good reason what you probably want to do is this:
static renderDetails(props, parentElement) {
if(props.length === 0) {
return null; //nothing to see here!
}
let coverageRows;
let children = props.map((columnData, index) => {
if (index === props.length - 1) {
coverageRows = (<TableRowCol classNames={styles.text_align_right}>{columnData}</TableRowCol>);
}
else {
coverageRows = (<TableRowCol>{columnData}</TableRowCol>);
}
return coverageRows;
})
parentElement.children = children
return <span>parentElment</span> //I'm not sure why you need the span, but if you do great. I would just return the parentElement
}
//Called by...
render() {
...
renderDetails(props, <TableRow />)//an actual table row instance, not the tag name as a string
...
}
I didn't test any of this but it should get you moving in the right direction. I would recommend writing a custom component that renders children so you understand how that works. It will save you a lot of time down the road.
Using React, I'm fetching data from an API, a sample of which can be seen here.
I need to loop through the body section (multi-dimensional array) and then determine what type of 'block' it is:
heading
text
quote
frameImgeBlock
quoteImageBlock
frameQuoteBlock
Depending which block type it is then I need to create/load the corresponding React component (as I'm imagining it's better to separate these into individual React components).
How is it best to approach this in React/JS?
I may be able to tweak the API output a little if someone can suggest an easier approach there.
You could use a "translation" function like so, which would call external components:
// Import external deps
import Heading from './src/heading';
// Later in your component code
function firstKey(obj) {
return Object.keys(obj)[0];
}
getDomItem(item) {
const key = firstKey(item);
const val = item[val];
switch (key) {
case "heading":
return <Heading heading={ val.heading } ...etc... />
case "other key..."
return <OtherElm ...props... />
}
}
render() {
// data is your object
return data.content.body.map(item =>
this.getDomItem(item[firstKey(item)])
);
}
This is not a question as much "how to make this work" as much as it is a "was this the best way." Here's my code:
/**
* React Static Boilerplate
* https://github.com/koistya/react-static-boilerplate
* Copyright (c) Konstantin Tarkus (#koistya) | MIT license
*/
import React, { Component } from 'react';
// import './InputWidgetText.scss';
import ContentBlock from '../ContentBlock';
var i = 0;
var contentBlocks = [];
var ContentContainer = React.createClass({
addNewBlock: function(){
i++;
contentBlocks.push(<ContentBlock key={i} index={i}/>)
this.forceUpdate();
},
render: function(){
if (this.props.inputs) {
contentBlocks = this.props.inputs.map(function(item, index){
i++;
return(<ContentBlock key={index} index={index} content={item} />)
});
}
return (
<div>
{contentBlocks}
<button onClick={this.addNewBlock}>+</button>
</div>
)
}
});
export {ContentContainer as default};
The problem is that every so often on a refresh the props.inputs are not getting passed down to this component and throwing an error when I try to map undefined. So the simple solution is to put the map process in an if check for whether or not the props are there yet - is that actually the right way to handle this? My data is passed in via a reflux mixin on the parent. I just feel like there might be a more proper way to handle this. Thanks for the feedback!
May I strongly suggest you refactor your code to do away with the file variables i and contentBlocks.
The contentBlocks variable seems completely unnecessary, whilst your i variable should be part of the state. Whilst you're at it, give i a more meaningful name, e.g. blockCount.
getInitialState: function () {
return {
blockCount: 0
};
},
Then define your click event handler to modify the state:
addNewBlock: function () {
this.setState({
blockCount: this.state.blockCount + 1
});
},
Every time you call setState(), React will trigger a re-render. You should never need to call forceUpdate().
Finally, your render() function should return its content based SOLELY on this.props and this.state. That is, for any given props and state, the output will be predictable. Think of this.props and this.state as input parameters to the render() function. That is all render() can, or needs to, know about.
I won't try to write the render() function as I'm not sure exactly what you're trying to achieve with this component. But for a given this.props.input and this.state.blockCount (or whatever you choose to use as props and state) you should know exactly what you're outputting.
I know I haven't directly answered the question you put, but I hope this clarifies some React concepts.
I need to resolve all React components into their respective React elements (e.g., type=div,ul,li), including all nested components that there may be. I'm doing it successfully already; however, I'm getting a warning about calling a React component directly. Also, it's quite possible I'm going about it wrong in the first place.
function resolveNestedDomElements(reactElement) {
// is there a better way to check if it's a component?
if (React.isValidElement(reactElement) && _.isFunction(reactElement.type)) {
// is there a better way to access a component's children?
reactElement = reactElement.type(reactElement.props);
// Warning: Something is calling a React component directly. Use a
// factory or JSX instead. See https://fb.me/react-legacyfactory
// Note: Unfortunately, the factory solution doesn't work.
}
if (!React.isValidElement(reactElement)) {
return reactElement;
}
let { children } = reactElement.props;
if (React.Children.count(children)) {
if (React.isValidElement(children)) {
children = resolveNestedDomElements(children);
} else {
children = React.Children.map(
children,
resolveNestedDomElements
);
}
}
return React.cloneElement(reactElement, reactElement.props, children);
}
Your original code now returns an error in 0.14.7, with no output, from React, containing the same message that used to warn (again, behavior as described in the link provided above).
Updating your code to this, as per the update here:
function resolveNestedDomElements(reactElement) {
if (React.isValidElement(reactElement) && typeof reactElement.type === 'function') {
reactClass = React.createFactory(reactElement.type)
reactElement = reactClass(Object.assign({},reactElement.props));
}
if (!React.isValidElement(reactElement)) {
return reactElement;
}
let { children } = reactElement.props;
if (React.Children.count(children)) {
if (React.isValidElement(children)) {
children = resolveNestedDomElements(children);
} else {
children = React.Children.map(
children,
resolveNestedDomElements
);
}
}
return React.cloneElement(reactElement, reactElement.props, children);
}
This no longer warns (or, in current versions, it now just returns an error in the original form you posted), but the output is not resolved, either. It's unclear to me why this has changed.
The answer seems to be that it seems impossible to resolve anymore. I can't quite follow what's happening in the React source (I believe the render functions of components eventually are called by ReactDOM.render these days, but how that works is unclear to me still). Calling ReactDOM.render doesn't work without a DOM. There is a react-test-utils.shallowRenderer that looks like it may be helpful, that I need to experiment with.
It's a bit absurd how difficult this is. I'm not sure what architecture decision I'm missing at this point.