groupBy json data then map in React component - reactjs

I'm trying to group my JSON data so that all staff in the same 'department' are in an object together. Then I want to map through the data so I can display it on my webpage.
At the moment i'm getting the error 'cannot read property of map undefined'
my JSON data:
people:[
{
id:"a0bef",
title:"cleaner",
department:"facilities",
},
{
id:"a0beg",
title:"maintenance",
department:"facilities",
},
{
id:"a0beh",
title:"cleaner",
department:"facilities",
},
{
id:"a0bei",
title:"chef",
department:"kitchen",
},
{
id:"a0bej",
title:"waitress",
department:"kitchen",
}
]
which I would like to look like:
people:[
"facilities":[
{
id:"a0bef",
title:"cleaner"
},
{
id:"a0beg",
title:"maintenance"
},
{
id:"a0beh",
title:"cleaner"
}
],
"kitchen":[
{
id:"a0bei",
title:"chef"
},
{
id:"a0bej",
title:"waitress"
}
]
]
this is what I have tried:
import React from 'react'
import _ from 'lodash'
class PeopleList extends React.Component {
constructor (props) {
super(props)
}
render () {
const { peoplelist } = this.props
const people = _.groupBy(peoplelist, 'department')
return (
<div>
{ Object.people.map(function (person, key) {
return (
<div className='row' key={key}>
<div className='col-md-6'>{person.department}</div>
<div className='col-md-4'>{person.title}</div>
</div>
)
}) }
</div>
)
}
}
export default PeopleList

You are getting this error because Object.people is not a valid syntax.
people after const people = _.groupBy(peoplelist, 'department') will be an Object. You need to get all the values of the object (using Object.values(people)); which will give you an array of person. Then, map through that array to get the desired output.
The function will be modified to
Object.values(people).map(function (deptArray) {
return deptArray.map(function (person) {
return (
<div className='row' key={some-unique-key}>
<div className='col-md-6'>{person.department}</div>
<div className='col-md-4'>{person.title}</div>
</div>
)
})
})
Note: In the above case you won't be able to use array indexes as the key, because the keys will be repeated (and so, I have removed it from my solution).
Hope it helps. Revert for any clarifications/doubts.

At the moment i'm getting the error 'cannot read property of map
undefined'
This is due to this line in your return statement { Object.people.map(function (person, key)
const people = _.groupBy(peoplelist, 'department')
Above will you an object of 2 arrays i.e facilities and kitchen.
So you can do the following for displaying the data.
Object.values(people).map(dept => {
dept.map((person, key) => {
return (
<div className='row' key={key}>
<div className='col-md-6'>{person.department}</div>
<div className='col-md-4'>{person.title}</div>
</div>
)
})
})

If you want to group your items by department and then display a list of persons ordered by department, then you need to map twice. Once over the departments, and then once over each person in each department.
To iterate over the departments, you will need to use Object.values() or Object.entries().
Here is an example using reduce() to implement groupBy:
const people = [
{ id: "a0bef", title: "cleaner", department: "facilities" },
{ id: "a0beg", title: "maintenance", department: "facilities" },
{ id: "a0beh", title: "cleaner", department: "facilities" },
{ id: "a0bei", title: "chef", department: "kitchen" },
{ id: "a0bej", title: "waitress", department: "kitchen" }
]
function groupBy(data, key) {
return data.reduce((acc, x) => {
acc[x[key]] = [...(acc[x[key]] || []), x];
return acc;
}, {});
}
class PeopleList extends React.Component {
constructor(props) {
super(props)
}
render() {
const people = groupBy(this.props.people, 'department')
return (
<div className='container'>
{Object.entries(people).map(([dep, staff]) => {
return (
<div className='dep' key={dep}>
<span className='dep-name'>{dep}</span>
{staff.map(person => {
return (
<div className='row' key={person.id}>
<div className='col-xs-6'>Dep: {person.department}</div>
<div className='col-xs-4'>Title: {person.title}</div>
</div>
)
})}
</div>
)
})}
</div>
)
}
}
ReactDOM.render(
<PeopleList people={people} />,
document.getElementById('root')
);
.dep {
border: 1px solid black;
margin: 5px;
padding: 5px;
}
.dep-name {
font-weight: bold;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
<div id="root"></div>

Related

How I can print nested array under value (object element) in React.js

I am trying to print a nested array (sublist) under object element (value) from the state.list. I tried but did not get the expected result I want. I made two components named Orderlist and Item which hold the nested array and value elements. I could not find where I am doing wrong. Sorry! I am in the learning stage and working on a super small project. Every help would be appreciated.
import React from "react";
import "./styles.css";
const Item = (props) => {
return props.list.map((item)=><li>{item.sublist}</li>);
};
const Orderlist = (props) => {
return props.list.map((element) => (
<ol>
{element.value}
<Item list = {props.list} />
</ol>
));
};
class App extends React.Component {
state = {
list: [
{ value: "Fruit", sublist: ["Banana", "Apple", "Graps"] },
{ value: "Vegetable", sublist: ["Carrat", "Potato", "Mushroom"] },
{ value: "Sports", sublist: ["Cricket", "Badminton", "Football"] },
{ value: "Continent", sublist: ["Asia", "Europe", "Africa"] }
]
};
render() {
return <Orderlist list={this.state.list} />;
}
}
export default App;
outcome got ........
Fruit
BananaAppleGraps
CarratPotatoMushroom
CricketBadmintonFootball
AsiaEuropeAfrica
Vegetable
BananaAppleGraps
CarratPotatoMushroom
CricketBadmintonFootball
AsiaEuropeAfrica
Sports
BananaAppleGraps
CarratPotatoMushroom
CricketBadmintonFootball
AsiaEuropeAfrica
Continent
BananaAppleGraps
CarratPotatoMushroom
CricketBadmintonFootball
AsiaEuropeAfrica
There were two mistakes:
You need to pass element.sublist as list prop to Item instead of props.list:
<Item list={element.sublist} />
List item should be just item instead of item.sublist:
<li>{item}</li>
const Item = (props) => {
return props.list.map((item, index) => <li key={index}>{item}</li>);
};
const Orderlist = (props) => {
return props.list.map((element, idx) => (
<ol key={idx}>
{element.value}
<Item list={element.sublist} />
</ol>
));
};
class App extends React.Component {
state = {
list: [
{ value: "Fruit", sublist: ["Banana", "Apple", "Graps"] },
{ value: "Vegetable", sublist: ["Carrat", "Potato", "Mushroom"] },
{ value: "Sports", sublist: ["Cricket", "Badminton", "Football"] },
{ value: "Continent", sublist: ["Asia", "Europe", "Africa"] }
]
};
render() {
return <Orderlist list={this.state.list} />;
}
}
ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class='react'></div>
https://codesandbox.io/s/dazzling-golick-0ie3qy?file=/src/App.js
{list.map((item, index) => (
<>
<li>{item.value}</li>
{item?.sublist?.map((subitem, index) => (
<>
<li>{subitem}</li>
</>
))}
</>
))}
try this :
const Item = (props) => {
return props.list.map((item)=><li>{item}</li>);
};
const Orderlist = (props) => {
return props.list.map((element) => (
<ol>
{element.value}
<Item list = {element.sublist} />
</ol>
))

change text of a specific button when clicked in React

I want to change the text of a specific button when I click on that button in React. But the issue is when I click the button the title will change for all buttons!
class Results extends Component {
constructor() {
super();
this.state = {
title: "Add to watchlist"
}
}
changeTitle = () => {
this.setState({ title: "Added" });
};
render() {
return (
<div className='results'>
{
this.props.movies.map((movie, index) => {
return (
<div className='card wrapper' key={index}>
<button className='watchListButton' onClick={this.changeTitle}>{this.state.title}</button>
</div>
)
})
}
</div>
)
}
}
You would need to come up with a mechanism to track added/removed titles per movie. For that, you would have to set your state properly. Example:
this.state = {
movies: [
{id: 1, title: 'Casino', added: false},
{id: 2, title: 'Goodfellas', added: false}
]
This way you can track what's added and what's not by passing the movie id to the function that marks movies as Added/Removed. I have put together this basic Sandbox for you to get you going in the right direction:
https://codesandbox.io/s/keen-moon-9dct9?file=/src/App.js
And here is the code for future reference:
import React, { Component } from "react";
import "./styles.css";
class App extends Component {
constructor() {
super();
this.state = {
movies: [
{ id: 1, title: "Casino", added: false },
{ id: 2, title: "Goodfellas", added: false }
]
};
}
changeTitle = (id) => {
this.setState(
this.state.movies.map((item) => {
if (item.id === id) item.added = !item.added;
return item;
})
);
};
render() {
const { movies } = this.state;
return (
<div className="results">
{movies.map((movie, index) => {
return (
<div className="card wrapper" key={index}>
{movie.title}
<button
className="watchListButton"
onClick={() => this.changeTitle(movie.id)}
>
{movie.added ? "Remove" : "Add"}
</button>
</div>
);
})}
</div>
);
}
}
export default App;

Accessing id field of query returned object changes it to undefined

Im working on a react/mondgodb/graphql/apollo web application. I have a query set up to get a member using a fbid (its a firebase Id that I insert as fbid in order to find a logged in member profile in my database by the firebase id). When I do the query, I can console.log the query returned data as a whole. If I try to access members of the data, I get that the object is undefined. Here is the component where I query:
import React, { Component } from 'react';
import { Redirect } from 'react-router-dom'
import { graphql, compose } from 'react-apollo';
import { withFirebase } from '../Firebase';
import { getMemberByFBidQuery } from '../../queries/queries';
import DataList from '../wishes/MyDataList';
const INITIAL_STATE = {
memberProfile: {},
error: null
}
class DashboardGuts extends Component {
constructor(props) {
super(props);
this.state = { ...INITIAL_STATE };
}
render() {
const memberProfile = this.props.getMemberByFBidQuery.memberByFBid;
console.log('memberProfile: ', memberProfile.id);
return (
<div className="dashboard container">
<div className="row">
<div className="col s12 m6">
<h4>My Data List</h4>
<DataList memberProfile={memberProfile} />
</div>
</div>
</div>
)
}
}
const Dashboard = withFirebase(DashboardGuts)
export default compose(
graphql(getMemberByFBidQuery, {
name: "getMemberByFBidQuery",
options: (props) => {
return {
variables: {
fbid: props.firebase.auth.O
}
}
}
}),
)(Dashboard)
When I console.log the memberProfile in the code above, I get all the data I expect returned from the query. The console.log output looks like the following:
memberProfile:
{firstname: "jo", lastname: "blo", id: "1234",
fbid: "5678", __typename: "Member"}
fbid: "5678"
firstname: "jo"
id: "1234"
lastname: "smith"
__typename: "Member"
__proto__: Object
}
The problem is when I try to access any of the fields within the memberProfile object - like id, I get errors. For example if I change this line:
console.log('memberProfile: ', memberProfile);
To this:
console.log('memberProfile: ', memberProfile.id);
Then I get the error that I can't access id of undefined object.
Any ideas on how to access that id property? In case it helps, here is the actual graphQL query:
const getMemberByFBidQuery = gql`
query($fbid: String){
memberByFBid(fbid: $fbid) {
firstname,
lastname,
id,
fbid
}
}
`
And here's the schema for that query:
memberByFBid: {
type: MemberType,
args: { fbid: { type: GraphQLString } },
resolve(parent, args) {
return Member.findOne({ fbid: args.fbid });
}
},
And here is the schema for MemberType:
const MemberType = new GraphQLObjectType({
name: "Member",
fields: () => ({
id: { type: GraphQLID },
firstname: { type: GraphQLString },
lastname: { type: GraphQLString },
email: { type: GraphQLString },
fbid: { type: GraphQLString },
wishes: {
type: GraphQLList(WishType),
resolve(parent, args) {
return Wish.find({ memberId: parent.id })
}
},
groups: {
type: GraphQLList(MemberToGroupMapType),
resolve(parent, args) {
return MemberToGroupMap.find({ memberId: parent.id });
}
}
})
});
Thanks in advance for any help!
Per TLadd's suggestions, I changed my render function but Im still getting error. Her it is currently:
render() {
console.log('look for loading: ', this.props);
const { memberByFBid, loading, error } = this.props.getMemberByFBidQuery;
if (loading) {
return (<p>Loading...</p>);
} else if (error) {
return (<p>Error!</p>);
}
console.log('memberProfilexx: ', memberByFBid);
return (
<div className="dashboard container">
<div className="row">
<div className="col s12 m6">
<h4>My Wish List</h4>
<WishList memberProfile={memberByFBid} />
</div>
<div className="col s12 m5 offset-m1">
<div>Hello</div>
</div>
</div>
</div>
)
}
But I still get the error if I try to console.log memberByFBid.id instead of just memberByFBid.
You are hitting this error because you query has not loaded yet when your DashboardGuts component first attempts to render. From the react-apollo docs on the data property (getMemberByFBidQuery in this case, since you specify a custom name config option in the graphql HOC):
Make sure to always check data.loading and data.error in your components before rendering. Properties like data.todos which contain your app’s data may be undefined while your component is performing its initial fetch. Checking data.loading and data.error helps you avoid any issues with undefined data.
Following this advice, your render method becomes something like
render() {
const { memberByFBid, loading, error } = this.props.getMemberByFBidQuery;
if (loading) {
return <p>Loading...</p>;
} else if (error) {
return <p>Error!</p>;
}
console.log('memberProfile: ', memberProfile.id);
return (
<div className="dashboard container">
<div className="row">
<div className="col s12 m6">
<h4>My Data List</h4>
<DataList memberProfile={memberProfile} />
</div>
</div>
</div>
)
}
You would likely want to handle loading/error state in a more appealing fashion, but the main idea is that you need to account for the data not being loaded yet in the render method, since it won't be in the initial render.

Object within state is changing without being prompted

I'm using setState to update a particular object but for some reason, another object is being mutated as well even though I haven't added it in the setState function.
Basically I have an 'add to cart' function, if item already exists in cart state object, just increments it's quantity by 1. If not, update state of cart with a copy that contains said item.
this.state = {
items: {
0: { name: 'Red shirt', price: 10 },
1: { name: 'Blue shirt', price: 11 },
2: { name: 'Green shirt', price: 12 },
3: { name: 'Yellow shirt', price: 13 }
},
cart: {},
user: {}
}
addToCart = (key, item) => {
let newCart;
newCart = this.state.cart;
// item already exists in cart
if (newCart[key]) {
// increment qty of added item
newCart[key].qty++;
// does not exist, need to add to cart
} else {
newCart[key] = item;
newCart[key].qty = 1;
}
this.setState({ cart: newCart });
}
render() {
const { cart, items } = this.state;
return (
<div className="App">
<h1>Streat</h1>
<Checkout cart={cart} />
<Items
items={items}
addToCart={this.addToCart}
/>
</div>
);
}
}
// Items component which holds Item component
class Items extends Component {
constructor(props) {
super(props)
}
render() {
const { items, addToCart } = this.props;
return (
<div>
{
map(items, function renderItems(item, key) {
return <Item
item={item}
itemRef={key}
key={key}
addToCart={addToCart} />
})
}
</div>
)
}
}
// Item functional component which has the add to cart button
const Item = (props) => {
const { item, itemRef, addToCart } = props;
return (
<div>
<p>{item.name}</p>
<p>{item.price}</p>
<p>{item.qty || ""}</p>
<button onClick={() => addToCart(itemRef, item)}>Add</button>
</div>
)
}
When inspecting the state of my items object in react developer tools, I see that every time I click the button to add an item to the cart, it does correctly add the item to the cart / update the quantity of the item but it also updates the item in the items object in my state, adding a 'qty' key/ value pair which is not what I want.
I'm using setState on my cart object but my items object is being changed too for some reason.
Try to update your state by using spread syntax so you don't get into this kind of trouble. Redux also uses that a lot. By doing it you don't mutate your object and create a clean copy of them.
Your code is not copying the item on line (newCart[key] = item;). Instead, it is putting a reference from the same item and by changing the qty in the next line you consequently update it as well in the items key.
const Item = ({ name, price, onClick }) =>
<div onClick={onClick}>
{name} {price}
</div>
class App extends React.Component {
constructor() {
super()
this.state = {
items: {
0: { name: 'Red shirt', price: 10 },
1: { name: 'Blue shirt', price: 11 },
2: { name: 'Green shirt', price: 12 },
3: { name: 'Yellow shirt', price: 13 }
},
cart: {},
user: {}
}
}
addToCart(key, item) {
const hasItem = this.state.cart[key]
this.setState({
...this.state,
cart: {
...this.state.cart,
[key]: {
...(hasItem ? this.state.cart[key] : item),
qty: hasItem ? this.state.cart[key].qty + 1 : 1,
},
},
})
}
render() {
const { cart, items } = this.state
return (
<div>
<div>Click to add:</div>
{Object.keys(items).map(key =>
<Item
{...items[key]}
key={key}
onClick={this.addToCart.bind(this, key, items[key])}
/>
)}
<div style={{ marginTop: 20 }}>
{Object.keys(cart).map(key =>
<div key={key}>{cart[key].name}:{cart[key].qty}</div>
)}
</div>
</div>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

React add class active on selected tab

I have the following:
var Tab = React.createClass({
getInitialState: function(){
return {
selected:''
}
},
activateTab: function(e) {
e.preventDefault();
$('.navigation--active').removeClass('navigation--active');
this.setState({selected : true});
},
render: function() {
var isActive = this.state.selected === true ? 'navigation--active': '';
return (
<li onClick={this.activateTab} className={isActive}>
<p>
{this.props.content}
</p>
</li>
);
}
});
var Tabs = React.createClass({
render: function() {
var tabs = [],
total = this.props.data.points.total,
handleClick = this.handleClick;
total.forEach(function(el, i){
tabs.push(
<Tab content = {el.name}
key = {i}/>
);
});
return (
<ul className="navigation">
{tabs}
</ul>
);
}
});
however it only works when you click once on every tab, if you click the second time on the same tab the class doesn't get added anymore
In this case, would be better move state management to parent component Tabs, and pass to child only props which you need to detect class name or set new state in parent
var Tab = React.createClass({
render: function() {
return <li
className={ this.props.isActive ? 'navigation--active': '' }
onClick={ this.props.onActiveTab }
>
<p>{ this.props.content }</p>
</li>
}
});
var Tabs = React.createClass({
getInitialState: function() {
return { selectedTabId: 1 }
},
isActive: function (id) {
return this.state.selectedTabId === id;
},
setActiveTab: function (selectedTabId) {
this.setState({ selectedTabId });
},
render: function() {
var total = this.props.data.points.total,
tabs = total.map(function (el, i) {
return <Tab
key={ i }
content={ el.name }
isActive={ this.isActive(el.id) }
onActiveTab={ this.setActiveTab.bind(this, el.id) }
/>
}, this);
return <ul className="navigation">
{ tabs }
</ul>
}
});
const data = {
points: {
total: [
{ id: 1, name: 'tab-1', text: 'text' },
{ id: 2, name: 'tab-2', text: 'text-2' },
{ id: 3, name: 'tab-3', text: 'text-2' }
]
}
}
ReactDOM.render(
<Tabs data={ data } />,
document.getElementById('container')
);
.navigation {}
.navigation--active {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="container"></div>
Necro poster here.
Cool answer up above!
In any way, here is the 2018 upgrade answer with recompose and styled-components. You can even make a HOC out of it for a joyful reusability!
https://codesandbox.io/s/1826454zl7
import React from "react";
import ReactDOM from "react-dom";
import { compose, withState, withHandlers } from "recompose";
import styled from "styled-components";
const enhancer = compose(
withState("selectedTabId", "setSelectedTabId", 1),
withHandlers({
isActive: props => id => {
return props.selectedTabId === id;
},
setActiveTab: props => id => {
props.setSelectedTabId(id);
}
})
);
const Tabs = enhancer(props => {
return (
<ul>
{props.data.map((el, i) => {
return (
<Tab
key={i}
content={el.name}
isActive={props.isActive(el.id)}
onActiveTab={() => props.setActiveTab(el.id)}
/>
);
})}
</ul>
);
});
const Tab = props => {
return (
<StyledLi isActive={props.isActive} onClick={props.onActiveTab}>
<p>{props.content}</p>
</StyledLi>
);
};
const StyledLi = styled.li`
font-weight: ${({ isActive }) => (isActive ? 600 : 100)};
cursor: pointer;
font-family: Helvetica;
transition: 200ms all linear;
`;
const data = [
{ id: 1, name: "tab-1", text: "text" },
{ id: 2, name: "tab-2", text: "text-2" },
{ id: 3, name: "tab-3", text: "text-2" }
];
const ExampleApp = () => <Tabs data={data} />;
ReactDOM.render(<ExampleApp />, document.getElementById("app"));
Basic idea is that you need to get selected index, map over the item on every click, compare selected index with all other indexes and return true to props of a needed component if the match is found.

Resources