I have an object containing several arrays like:
const Items = {
Deserts: [{name:cookies}, {name:chocolate}],
Fruits: [{name:apple}, {name:orange}]
...
}
I want to render it as:
<title>Deserts</title>
Cookies
Chocolate
<title>Fruits</title>
Apple
Orange
So first I render the type:
return <Grid>
{Object.keys(Items).map(type => {
return <Box key={type}>
{type} // <== this would be the title, Fruits or whatever
{this.createCard(Items[type])}
</Box>
})}
</Grid>
Then I want to add the content of each type:
createCard = (items) => {
return <Box>
{items.forEach(item => {
return <div>{item.name}</div>
})}
</Box>
}
Content is not returned, it works fine if instead of a forEach loop I just add some predefined content.
The forEach method only iterates over all items but does not return anything. Instead, what you want to use is a map.
Also, make sure you wrap your return value when it extends more than one line:
createCard = (items) => {
return (<Box>
{items.map(item => {
return <div>{item.name}</div>
})}
</Box>);
}
If you don't do that it works as if a semicolon was introduced after the first line. So, in reality, your current code is equivalent to:
createCard = (items) => {
return <Box>;
// The code below will never be executed!
{items.forEach(item => {
return <div>{item.name}</div>
})}
</Box>
}
When returning an element you need to wrap it in parentheses, ( ) and I generally use map instead of forEach.
const createCard = items => {
return (
<Box>
{items.map(item => {
return ( <div>{item.name}</div> )
})}
</Box>
)
}
I believe you can also nix the curly braces if the function doesn't need any logic.
const createCard = items => (
<Box>
{items.map(item => {
return ( <div>{item.name}</div> )
})}
</Box>
)
-- Edit --
Now that I'm reading back over your question a much cleaner way to approach this would be to declare the component function outside of your class like
class Grid extends react.Component {
render(){
return (
<Box>
<CreateCard props={ item }/>
</Box
)
}
}
const CreateCard = props => (
<Box>
{props.items.map(item => {
return ( <div>{item.name}</div> )
})}
</Box>
)
Related
I have a React component as shown. I am passing prop hasItems and based on this boolean value, i am showing PaymentMessage Component or showing AddItemsMessage component.
export const PayComponent = ({
hasItems
}: props) => {
return (
<Wrapper>
{hasItems ? (
<PaymentMessage />
) : (
<AddItemsMessage />
)}
<Alerts
errors={errors}
/>
</Wrapper>
);
};
This works well. Now, i need to pass another prop (paymentError). So based on this, i modify the JSX as below. I will highlight the parts i am adding by using comment section so it becomes easy to see.
export const PayComponent = ({
hasItems,
paymentError //-----> added this
}: props) => {
return (
<Wrapper>
{!paymentError ? ( //----> added this. This line of code errors out
{hasItems ? (
<PaymentMessage />
) : (
<AddItemsMessage />
)}
):( //-----> added this
<Alerts
errors={errors}
/>
) //-----> added this
</Wrapper>
);
};
Basically, i am taking one more input prop and modifying the way my JSX should look. But in this case, i am not able to add one boolean comparison one after the error. How do i make it working in this case. Any suggestions please ???
I recommend you to create a function to handle this behavior. It's easier to read and to mantain
export const PayComponent = ({
hasItems,
paymentError
}: props) => {
const RenderMessage = () => {
if (hasItems) {
if (paymentError) {
return <PaymentMessage />
}
return <AddItemsMessage />
}
return <Alerts errors={errors}/>
};
return (
<Wrapper>
<RenderMessage />
</Wrapper>
);
};
I am trying to create a reusable carousel using react-bootstrap, i could create that one"
const ReusableCarousel = (props) => {
return (
<Carousel className="mt-4 border">
{props.items.map((item, index) => {
return (
<Carousel.Item key={index}>
<Row>
{props.children}
</Row>
</Carousel.Item>
);
})}
</Carousel>
);
}
Now as you see it is reusable till the point of the carousel item, props.children may represent multiple elements per one slide or single element per slide, but i can not achieve that according to my logic
in the parent:
<ReusableCarousel items={categories}> //categories is an array of arrays of objects
{
//item prop should be passed here to carouselItemCategories component
//but i couldn't find a way to achieve that
<CarouselItemCategories key={i} />
}
</ReusableCarousel>
carouselItemCategories Component:
const CarouselItemCategories = (props) => {
//still in my dreams
const { item } = props;
return (
<>
{
item.map((c, index) => {
return (
<Col key={index}>
//a category card here
</Col>
);
})
}
</>
);
}
Now i know what makes it work, it is about passing item prop(which represent an array of objects represents fraction of my categories) but i could not find any way to achieve that
you can imagine categories like that:
const categories = [
[
{
title: 'Laptops',
background: 'red'
},
{
title: 'Tablets',
background: 'blue';
}
],
[
{
title: 'Mouses',
background: 'yellow'
},
{
title: 'Printers',
background: 'orange';
}
]
]
If I understand you correctly, you want to use each of the items from your ReusableCarousel to generate a new CarouselItemCategories with the individual item passed in as a prop?
If so, you may want to take a look at the cloneElement function. Effectively, inside your mapping of the items prop, you would create a clone of your child element, and attach the individual item as a prop to that clone. Something like this:
const ReusableCarousel = (props) => {
return (
<Carousel className="mt-4 border">
{props.items.map((item, index) => {
return (
<Carousel.Item key={index}>
<Row>
{React.cloneElement(props.children, { item })}
</Row>
</Carousel.Item>
);
})}
</Carousel>
);
}
I just found another solution by using react context, i created a CarouselContext module :
import React from 'react';
const CarouselContext = React.createContext([]);
export const CarouselProvider = CarouselContext.Provider;
export default CarouselContext
and then in the ReusableCarousel component:
import { CarouselProvider } from './carouselContext'
const ReusableCarousel = (props) => {
return (
<Carousel >
{props.items.map((item, index) => {
return (
<Carousel.Item key={index} >
<Row >
{
<CarouselProvider value={item}>
{props.children}
</CarouselProvider>
}
</Row>
</Carousel.Item>
);
})}
</Carousel>
);
}
and then using the context to get item global variable
const CarouselItemCategories = () => {
const item = useContext(CarouselContext);
return (
<>
{
item.map((c, index) => {
return (
<Col>
//category card here
</Col>
);
})
}
</>
);
}
I've got the following code where I'm trying to loop through the array of objects, but for some reason it's telling me data.map is not a function, even though I know data has content in it.
function Tree({ data }) {
let count = 0;
return (
<>
{data.map(item => {
count += 1;
console.log("Item", item.name);
return (
<div key={count} className="node">
{/* {item.name} */}
</div>
);
})}
</>
);
}
export default Tree;
When I change it over to Object.entries, I get stuff back, but that's not how I've done this in the past so I'm a little confused on the issue here.
function Tree({ data }) {
let count = 0;
return (
<>
{Object.entries(data).map(item => {
count += 1;
console.log("Item", item.name);
return (
<div key={count} className="node">
{/* {item.name} */}
</div>
);
})}
</>
);
}
export default Tree;
The array object is constructed like so:
[
{
id: 0,
name: Brannon,
},
{
id: 1,
name: John,
}
]
Console log data. You will get this message when the data you are trying to map is not an array. When you run Object entries you are turning the object into an array consisting of its keys and values. So this implies that the original data is just a plain object and not an array.
Also just a tip map will auto handle index for you if you pass it as a second parameter instead of manually handling it as you are.
{Object.entries(data).map( (item, count) => {
//count += 1; remove this it will auto increment
console.log("Item", item.name);
return (
<div key={count} className="node">
{/* {item.name} */}
</div>
);
})}
Try,and write console.log output of data if it not help
function Tree({ data }) {
console.log("data", JSON.stringify(data));
return (
<>
{ data && data.map((item, index) => {
console.log("Item", item.name);
return (
<div key={index} className="node">
{/* {item.name} */}
</div>
);
})}
</>
);
}
export default Tree;
I have wrapped for both Query and Mutations so I can globally handle the repeat actions that need to happen with each Query, Mutation. In the query I transform the data so I don't need to worry about all the nodes, edges, etc
I am using react-adopt to wrap all my query and mutations components into one render prop back on the view layer.
Works - Page will re-render once a mutation has taken place
<ApolloQuery>
export const ApolloQuery = ({
query: query,
render,
}) => {
return (
<Query query={query}>
{({ data }) => {
return (
<Fragment>
render(data)
</Fragment>
)
}}
</Query>
)
}
A Component
export default externalProps => {
return (
<QueryContainer {...externalProps}>
{({ someQueryData, aMutation }) => { //react-adopt render props
const { nestedData } = new apolloClass(someQueryData).start()
return (
<Grid container spacing={16}>
{nestedData.map((ticket, index) => (
{...Mutation button in here}
))}
</Grid>
)
}}
</QueryContainer>
)
}
Does not work - Page does not re-render but cache is updated with correct records
<ApolloQuery>
<Query query={query}>
{({ data }) => {
const transformedData = new apolloClass(data).start() //move transform into render
return (
<Fragment>
render(transformedData)
</Fragment>
)
}}
</Query>
A Component
export default externalProps => {
return (
<QueryContainer {...externalProps}>
{({ someQueryData: { nestedData }, aMutation }) => {
return (
<Grid container spacing={16}>
{nestedData.map((ticket, index) => (
{...Mutation button in here}
))}
</Grid>
)
}}
</QueryContainer>
)
}
So now, the page will not update after a mutation if I move the apolloClass to transform before the render of the query
Most likely you need to set refetchQueries or awaitRefetchQueries in the mutation options to force Apollo updating those queries and hence triggering a re-render.
I'm learning react and I try to create simple TODO based on material-ui, I have problem with handling IconMenu menu actions, menu is displayed in listItem element. At this moment I have no idea how trigger deleteItem function with item name as a parameter when delete action is clicked in menu.
const iconButtonElement = (
<IconButton touch={true} tooltip="More" tooltipPosition="bottom-left">
<MoreVertIcon color="black"/>
</IconButton>
);
const rightIconMenu = (
<IconMenu iconButtonElement={iconButtonElement}>
<MenuItem value="done" leftIcon={<Done />}>Mark as done</MenuItem>
<MenuItem value="delete" leftIcon={<Delete />}>Delete</MenuItem>
</IconMenu>
);
class TodoElements extends Component {
deleteItem(nameProp)
{
this.props.delete(nameProp);
}
render() {
var listItemRender = function(item) {
return <ListItem key={item.name} primaryText={item.name} style={listItemStyle} rightIconButton={rightIconMenu}/>
};
listItemRender = listItemRender.bind(this);
return (
<List>
{this.props.items.map(listItemRender)}
</List>
)
}
}
As far as I can see, you should be able to add an onChange handler to your IconMenu. So your rightIconMenu can look like this:
const RightIconMenu = ({onChange}) => (
<IconMenu iconButtonElement={iconButtonElement} onChange={onChange}>
<MenuItem value="done" leftIcon={<Done />}>Mark as done</MenuItem>
<MenuItem value="delete" leftIcon={<Delete />}>Delete</MenuItem>
</IconMenu>
);
Then you can use it in your TodoElements like this:
class TodoElements extends Component {
constructor(props){
super(props);
this.state = {
items: props.items
};
}
createChangeHandler = (nameProp) => {
return (event, value) => {
if(value==="delete"){
this.deleteItem(nameProp);
}
};
}
deleteItem = (nameProp) =>
{
this.setState({
items: this.state.items.filter((item) => {
return item.name !== nameProp);
})
});
}
render() {
return (
<List>
{this.state.items.map((item) => {
<ListItem key={item.name} primaryText={item.name} style={listItemStyle}
rightIconButton={<RightIconMenu onChange={this.createChangeHandler(item.name)} />}/>
})}
</List>
)
}
}
Alternative
As an alternative solution you could bind an onClick handler to your delete MenuItem instead. I would probably implement it like this:
const RightIconMenu = ({onDelete}) => (
<IconMenu iconButtonElement={iconButtonElement}>
<MenuItem value="done" leftIcon={<Done />}>Mark as done</MenuItem>
<MenuItem value="delete" leftIcon={<Delete />} onClick={onDelete}>Delete</MenuItem>
</IconMenu>
);
And then replace the appropriate functions in the TodoElements:
createChangeHandler = (nameProp) => {
return (event, value) => {
this.deleteItem(nameProp);
};
}
render() {
return (
<List>
{this.state.items.map((item) => {
<ListItem key={item.name} primaryText={item.name} style={listItemStyle}
rightIconButton={<RightIconMenu onDelete={this.createDeleteHandler(item.name)} />}/>
})}
</List>
)
}
As for handling the state of your list of items, you should probably take a look at global state management such as Redux.
I think that a nicer approach would be using the onTouchTap every MenuItem has, So the onChange function won't have a switch or many if statements.
I'm actually using it when I iterate over all menu items,
To me it looks like this:
_.map(menuItems, (currItem, index) => {
return (<MenuItem primaryText={currItem.primaryText}
rightIcon={currItem.rightIcon}
leftIcon={currItem.leftIcon}
key={`menu-item-${index}`}
value={currItem.value}}
onTouchTap={currItem.onTouchTap}/>)
})