JSX get Component by string - javascript

This is pretty complicated to explain.
I am getting a couple of Icons from a component library as follows:
import { Icons } frommy-component-library';`
Let say I've got 3 Icons in there called IconPlus, IconMinus and IconEquals.
I can easily display the IconEquals along with a description prop as follows:
const IconContainer = ({ description ) => (
<div>
{description}
<Icons.IconEquals />
</div>
)
This works nicely. Now I'm trying to implement a template where I could pass another prop icon to this container which would display the corresponding icon.
Eg. if icon is IconPlus
Then it should render the following:
<div>
{description}
<Icons.IconEquals />
</div>
How do I implement my JSX to do that?
This is basically what I've got:
import { Icons } from 'my-component-library'
const IconContainer = ({ description, icon }) => (
<div>
{description}
<Icons.{icon} /> // <---- Obviously that doesn't work
// ^^^^ But I need something like this
</div>
);
Is it possible to do this?

You can use like this :
<Icons[icon] />

Related

React: Child Components Failed to Receive Multiple Mapped Data Using Props

I'm new to react. I want to create a simple react web app that receives data from 2 api, and display the data.
I have three components. App.js receive data from two api, and pass the data to FruitGrid.js using hooks and props. FruitGrid.js map the two data received, and pass to FruitItem.
My problem is in FruitGrid.js. Data is received in FruitGrid.js, I can see by console log or print using html tags. But when I try to send the mapped data to FruitItem.js by changing the h1 tag to <FruitItem></FruitItem>, I can only successfully pass one data, not both of them.
FruitGrid.js
import React from 'react';
import FruitItem from './FruitItem';
const FruitGrid = ({ items, images, isLoading }) => {
return isLoading ? (
<h1>Loading...</h1>
) : (
<section>
{items.map((item) => {
return <FruitItem key={item.id} item={item}></FruitItem>
})}
{images.map(image => {
return <FruitItem key={image.id} image={image}></FruitItem>
// return <h1>{image}</h1>
})}
</section>
)
}
export default FruitGrid;
If I only do return <FruitItem key={item.id} item={item}></FruitItem> the item data will show in the correct layout, image won't show since I didn't pass. But if I try to pass both item and image using <FruitItem></FruitItem>. It will show error saying "TypeError: Cannot read property 'name' of undefined" in FruitItem.
FruitItem.js
import React from 'react'
const FruitItem = ({ item, image }) => {
return (
<div className='card'>
<div className='card-inner'>
<div className='card-front'>
<img src={image} alt='' />
</div>
<div className='card-back'>
<h1>{item.name}</h1>
<ul>
</ul>
</div>
</div>
</div>
)
}
export default FruitItem
Can someone help and let me know how i can fix it?
Hey #yywhocodes this is happening because the FruitItem.js is always expecting an item object and your code for the images mapping is not providing the item object -> return <FruitItem key={image.id} image={image}></FruitItem>
What you can do is change the FruitItem.js
from
<h1>{item.name}</h1>
to
{ item ? <h1>{item.name}</h1> : null }
like that it will try to render the item.name only if the item exists.
You are iterating over the two data sources separately, so items are not defined in your images.map and images are not defined in your items.map
In order to combine, assuming the order of the data is the same/the keys are the same in both cases, you could do something like this:
{items.map((item, key) => {
return <FruitItem key={item.id} item={item} image={images[key]}></FruitItem>
})}
You need to pass props key={item.id} item={item} to the child FruitItem component. If you can't pass any props to the FruitItem component, react won't figure out the item.name. Which will be a TypeError.
It's because your FruitItem always expects an item to be handed in
<h1>{item.name}</h1>
But when you hand in your image, there is no item you set.

Search bar stopping props from changing

On my site, the <ArticleList> is supposed to update when one navigates between columns. This works when you go from the home page to a column, or from an article to a column. But if you go from column to column, it doesn't work. The page doesn't update at all, but the url changes. The links to each column stay the same, as they are part of the <Layout> component, which every page has.
Edit
I figured out now that I can just use <a> and omit <Link> entirely, but this would slow down the page navigation.
Edit 2
This is part of my <Layout> component where I render the links to the columns:
<nav className={layout.columnContainer}>
{columns.map(({ id, name }) =>
this.props.currentColumn ? (
<a key={id} href={`/columns/${name}`}>
{name}
</a>
) : (
<Link key={id} href="/columns/[name]" as={`/columns/${name}`}>
<a>{name}</a>
</Link>
),
)}
</nav>
Edit 3
My minimal reproducible example is on GitHub, and I get the same unexpected results.
Edit 4
I found that the reason it wasn't working was I implemented a search bar that put the children prop in a state and modified this.
Constructor:
constructor(props) {
super(props);
this.searchArticlesKeyType = this.searchArticlesKeyType.bind(this);
this.state = {displayedMain: props.children};
}
Inside the render method are the column links (nav) and the problematic search input element.
<nav className={layout.columnContainer}>
{
columns.map(({id, name}) => (
<Link key={id} href="/columns/[name]" as={`/columns/${name}`}><a>{name}</a></Link>
))
}
</nav>
<div className={layout.search}>
<input type="search" name="q" onKeyUp={this.searchArticlesKeyType} />
</div>
async searchArticlesKeyType(e) {
// Some code
this.setState({
displayedMain: <ArticleList articles={JSON.stringify(filteredArticles)}/>
// More code
});
}
I think your main issue here is the way you're implementing the search feature, you don't want to store components in state instead you need to pass the search text to the articlelist component and do the filtering there.
There are several ways to implement communication between 2 unrelated components, it could be via context, redux, or even make a portal in the layout to render the seach input from the column component, but in this case I think the best option is to store the search text in the url:
First make the input event update the url using next/router, your layout will look like this:
import { useRouter } from 'next/router'
...
function Layout(props) {
const {columns} = props;
const { push, asPath, query } = useRouter()
const searchArticlesKeyType = (e) => {
const q = e.target.value;
const [url] = asPath.split('?');
push(`${url}?q=${q}`, undefined, { shallow: true });
}
return (
<div>
...
<div>
<input type="search" name="q" defaultValue={query.q} onKeyUp={searchArticlesKeyType} />
</div>
...
</div>
)
}
And then you do the filtering in articlelist component
import Link from "next/link";
import { useRouter } from 'next/router'
export default function ArticleList(props) {
const { query } = useRouter();
const q = query.q || "";
const filteredArticles = props.articles.filter(
(item) => item.title.includes(q) || item.body.includes(q)
);
return (
<ul className="grid">
{filteredArticles.map((item) => (
<div key={item.id}>
<Link
key={item.id}
href="/articles/[title]"
as={`/articles/${item.title}`}
>
<a>
<p>
<strong>{item.title}</strong>
</p>
<p>{item.body.substring(0, 100)}</p>
</a>
</Link>
</div>
))}
</ul>
);
}

Can I have Component interfaces in React?

Say I have a <Modal> that takes a <Header> <Content> and <Footer>.
(
<Modal>
<Header>Foo</Header>
<Content>Foo</Content>
<Footer>Foo</Footer>
</Modal>
)
Now, inside my Modal component I'll probably have code like the following:
const header = children.find(child => child.type === Header)
In order to get a reference to the rendered header.
Now, what if from the consumer of the modal, I needed a decorated Header. Let's just call it DecoratedHeader
// DecoratedHeader
const DecoratedHeader = () => <Header>Foo <Icon type="lock" /></Header>
// consumer
(
<Modal>
<DecoratedHeader />
<Content>Foo</Content>
<Footer>Foo</Footer>
</Modal>
)
The line above wouldn't work anymore, as DecoratedHeader type is not Header. However, it IS rendering a Header.
It feels like there's the concept of "interface" which is missing. Ultimately, the Modal cares for a Header to be rendered, but if you wrap it under a "custom" component there's no way for it to know that it is still a Header.
What am I missing?
EDIT
To expand more about my use cases, I don't need an alternative solution. I need to know whether React has support for a mechanism equivalent to an interface, where 2 different Components that comply with the Liskov Substitution Principle (meaning they're swappable) can have a way to be picked by the parent.
Specifically, replacing this "hardcoded implementation" search, with an "interface" search:
-const specificChild = children.find(child => child.type === SomeComponent)
+const componentInterface = children.find(child => ????)
// Get a prop out of that component interface
const { someInterfaceProp } = componentInterface.props;
return (
<div>
{componentInterface} {/* render it on a specific place */}
</div>
)
Assuming the only thing you're going to be doing with these components is rendering them in specific spots of the modal, i would do them as separate props. For example:
const Modal = ({ header, content, footer }) => {
return (
<div>
{header}
<SomethingElseAllModalsHave />
{content}
{footer}
</div>
)
}
// ... used like:
const Example = () => {
return (
<Modal
header={<DecoratedHeader />}
content={<Content>Foo</Content>}
footer={<Footer>Foo</Footer>}
/>
)
}
If you need the modal to not just render the other components, but give them some information too, you could use a render prop. Basically the same as my example above, but now you pass in functions instead of elements
const Modal = ({ header, content, footer }) => {
const [isVisible, setIsVisible] = useState(false);
return (
<div>
{header(isVisible)}
<SomethingElseAllModalsHave />
{content(isVisible)}
{footer(isVisible}
</div>
)
}
// ... used like:
const Example = () => {
return (
<Modal
header={() => <DecoratedHeader />}
content={(isVisible) => <Content>{isVisible ? "Foo" : "Bar"</Content>}
footer={(isVisible) => isVisible ? <Footer>Foo</Footer> : null}
/>
)
}
EDIT:
When you write the JSX <DecoratedHeader/>, the object that is produced contains no information about <Header>. It's basically just an object with a type (ie, a reference to DecoratedHeader) and some props (none in this case). Header only enters the picture when DecoratedHeader is rendered, which won't be until after Modal is rendered.
So whatever the characteristics are that Modal will use to identify what is and is not a header, it needs to be something that is on DecoratedHeader, not just on Header. Perhaps you could add a static property to any component that counts as a header, and then check for that:
const Header = () => {
// Whatever the code is for this component.
}
Header.isHeader = true;
const DecoratedHeader = () => <Header>Foo <Icon type="lock" /></Header>
DecoratedHeader.isHeader = true;
Then you'll look for it something like this (you should use React.Children, because children is not guaranteed to be an array):
const header = React.Children.toArray(children).find(child => child.type.isHeader);

How to render the component from property of object?

I am building a navbar which contains few icons and titles.
To build it, I am using react-icons (https://www.npmjs.com/package/react-icons).
So, I am importing these items here
import { FaMobileAlt, FaCreditCard, FaRegBuilding } from 'react-icons/fa';
and I have a const responsible for keep the menu items list
const LEFT_MENU_ITEMS = [
{ key: 'devices', icon: FaMobileAlt, title: 'Devices' },
{ key: 'cards', icon: FaCreditCard, title: 'Cards' },
{ key: 'business', icon: FaRegBuilding, title: 'Business' },
];
The normal usage would be something like <FaMobileAlt /> and thats it, but in my case I am trying to iterate over this const to build my list.
buildLeftMenuBar() {
if (!this.props.loggedIn) return null;
return (
<ul key="leftMenuBar" className="items">
{_.map(LEFT_MENU_ITEMS, itemDef => (
<li key={itemDef.key}>
<NavLink to={`/${itemDef.key}`}>
<div>
>>>>> {itemDef.icon} <<<<
</div>
<span>{itemDef.title}</span>
</NavLink>
</li>
))}
</ul>
);
}
The error I get when render the page is
Functions are not valid as a React child. This may happen if you return a Component instead of from render.
So, each icon isnt being rendered. How can I make it work following the same idea ?
ps: It's not only 3 items, I just removed some to make it easier to read the question, thats why I am trying to iterate.
You may use like this:
<itemDef.icon />
Instead of this:
{itemDef.icon}
Or, you may also use like:
{itemDef.icon()}
You might be wondering what's going in here. So, let me explain a little bit:
When you want to render a component say MyComponent, you will be able to print like:
{MyComponent()}
Or,
<MyComponent />
But not like:
{MyComponent}
Because, you will need to call the function. That's it.
As per your comment, you want to supply the size props in the component as you said:
<itemDef.icon size={20} />
And {itemDef.icon()} is not just limited. You can also pass the props here:
{itemDef.icon({size:20})} // component receives size props 20
Though, I would recommend to use <itemDef.icon size={20} /> because it's little bit clear usage.
You can do it like this:
return(
<ul key="leftMenuBar" className="items">
{_.map(LEFT_MENU_ITEMS, item => {
const Icon = item.icon;
return (
<li key={itemDef.key}>
<NavLink to={`/${itemDef.key}`}>
<Icon />
<span>{itemDef.title}</span>
</NavLink>
</li>
);
})}
</ul>
);

React pattern for List Editor Dialog

I'd like to know what's the best pattern to use in the following use case:
I have a list of items in my ItemList.js
const itemList = items.map((i) => <Item key={i}></Item>);
return (
<div>{itemList}</div>
)
Each of this Items has an 'EDIT' button which should open a dialog in order to edit the item.
Where should I put the Dialog code?
In my ItemList.js => making my Item.js call the props methods to open the dialog (how do let the Dialog know which Item was clicked? Maybe with Redux save the id of the item inside the STORE and fetch it from there?)
In my Item.js => in this way each item would have its own Dialog
p.s. the number of items is limited, assume it's a value between 5 and 15.
You got a plenty of options to choose from:
Using React 16 portals
This option let you render your <Dialog> anywhere you want in DOM, but still as a child in ReactDOM, thus maintaining possibility to control and easily pass props from your <EditableItem> component.
Place <Dialog> anywhere and listen for special app state property, if you use Redux for example you can create it, place actions to change it in <EditableItem> and connect.
Use react context to send actions directly to Dialog, placed on top or wherever.
Personally, i'd choose first option.
You can have your <Dialog/> as separate component inside application's components tree and let it to be displayed in a case if your application's state contains some property that will mean "we need to edit item with such id". Then into your <Item/> you can just have onClick handler that will update this property with own id, it will lead to state update and hence <Dialog/> will be shown.
UPDATED to better answer the question and more completely tackle the problem. Also, followed the suggestion by Pavlo Zhukov in the comment below: instead of using a function that returns functions, use an inline function.
I think the short answer is: The dialog code should be put alongside the list. At least, this is what makes sense to me. It doesn't sound good to put one dialog inside each item.
If you want to have a single Dialog component, you can do something like:
import React, { useState } from "react";
import "./styles.css";
const items = [
{ _id: "1", text: "first item" },
{ _id: "2", text: "second item" },
{ _id: "3", text: "third item" },
{ _id: "4", text: "fourth item" }
];
const Item = ({ data, onEdit, key }) => {
return (
<div key={key}>
{" "}
{data._id}. {data.text}{" "}
<button type="button" onClick={onEdit}>
edit
</button>
</div>
);
};
const Dialog = ({ open, item, onClose }) => {
return (
<div>
<div> Dialog state: {open ? "opened" : "closed"} </div>
<div> Dialog item: {JSON.stringify(item)} </div>
{open && (
<button type="button" onClick={onClose}>
Close dialog
</button>
)}
</div>
);
};
export default function App() {
const [isDialogOpen, setDialogOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState(null);
const openEditDialog = (item) => {
setSelectedItem(item);
setDialogOpen(true);
};
const closeEditDialog = () => {
setDialogOpen(false);
setSelectedItem(null);
};
const itemList = items.map((i) => (
<Item key={i._id} onEdit={() => openEditDialog(i)} data={i} />
));
return (
<>
{itemList}
<br />
<br />
<Dialog
open={isDialogOpen}
item={selectedItem}
onClose={closeEditDialog}
/>
</>
);
}
(or check it directly on this CodeSandbox)

Categories