I am new to React and dev in general, but I am struggling to figure out how to achieve what I am trying to do. I feel as though I may have missed something along the way.
My goal is to have a list of items, that which on clicked individually, will toggle the visibility of their information.
The problem is that I am not able to map over the state in the parent element to display each object. But the state is in an array so I don't understand why it wouldn't be iterable. I do not get this problem when its just an object that I pass props to the child without state.
Is this the correct way to go about doing this? Am I supposed to create another array just to map over my object? I've also been a little confused as some sources create a class and use the constructor and render function. Is that deprecated or should I be doing it this way?
Parent
import React from "react";
import { useState } from "react";
//Components
import Card from "./Card";
const CardStack = () => {
const [habits, setHabits] = [
{
id: 1,
merit: "good",
title: "Good Habit",
count: 4,
text: "Words to be hidden",
visible: false,
},
{
id: 2,
merit: "bad",
title: "Bad Habit",
count: 1,
text: "Words to be hidden",
visible: false,
},
{
id: 3,
merit: "good",
title: "Good Habit",
count: 6,
text: "Words to be hidden",
visible: true,
},
];
const toggleCard = () => {
this.setHabits((habit) => {
habit.visible = !visible;
});
};
return (
<div className="card-stack">
{habits.map((habit) => (
<Card habit={habit} key={habit.id} onClick={toggleCard} />
))}
</div>
);
};
export default CardStack;
Child
import React from "react";
//Components
import Button from "./Button";
const Cards = ({ habit, onClick }) => {
return (
<div className="card" key={habit.id} onClick={onClick}>
<h4 className="title" merit={habit.merit}>
{habit.title}
<div className="btn-group">
<Button className="button" />
<span className="count">{habit.count}</span>
<Button className="button" />
</div>
{habit.visible ? (
<div className="content">
<p>visible</p>
</div>
) : null}
</h4>
</div>
);
};
export default Cards;
There are a number of problems with your code.
The first has been pointed out by #talfreds in their answer – you need to call useState() to initialize the state variable and its corresponding setter.
const CardStack = () => {
const [habits, setHabits] = useState([
{
id: 1,
merit: "good",
title: "Good Habit",
count: 4,
text: "Words to be hidden",
visible: false,
},
...]);
Just doing this should allow your component to render.
But once you click the button, your current toggle handler will overwrite the array stored in habits with a boolean.
To fix this you need to understand that the callback you pass to setState is passed the current value of the relevant state variable for you to work with, and the state will be set to the value that you return from the callback. When working with arrays you need to avoid directly mutating this passed value, in this example by using map() which returns a new array, and by cloning the 'habit' object that we are changing use spread syntax.
const toggleCard = (id) => { // pass the id of the 'habit' to toggle
setHabits((habits) => { // the current 'habits' array is passed to the callback
// return a new array and avoid mutating nested objects when updating it
return habits.map((habit) => habit.id === id ? { ...habit, visible: !habit.visible } : habit);
});
};
// usage
{habits.map((habit) => (
...
<button type="button" onClick={() => toggleCard(habit.id)}>Toggle</button>
...
)}
The last glaring problem is your use of this which is necessary when working with a class based component, but isn't necessary in a function component and actually won't work at all in the context of an arrow function.
Here is a shortened example snippet that may help you working through these ideas.
const { useEffect, useState } = React;
const App = () => {
const [ habits, setHabits ] = useState([ // call useState to initialize 'habits' state
{
id: 1,
merit: 'good',
title: 'Good Habit',
count: 4,
text: 'Words to be hidden',
visible: false,
},
{
id: 2,
merit: 'bad',
title: 'Bad Habit',
count: 1,
text: 'Words to be hidden',
visible: false,
},
{
id: 3,
merit: 'good',
title: 'Good Habit',
count: 6,
text: 'Words to be hidden',
visible: true,
},
]);
useEffect(() => {
console.log('This: ', this);
}, []);
const toggleCard = (id) => { // id passed from mapped buttons
setHabits((habits) => { // the current 'habits' array is passed to the callback
// return a new array and avoid mutating nested objects when updating it
return habits.map((habit) => habit.id === id ? { ...habit, visible: !habit.visible } : habit);
});
};
return (
<div className="card-stack">
{habits.map((habit) => (
<div key={habit.id} className="card">
<h3>{habit.title}</h3>
{habit.visible
? (<p>{habit.text}</p>)
: null}
<button type="button" onClick={() => toggleCard(habit.id)}>Toggle</button>
</div>
))}
</div>
);
};
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Looks like you forgot to call useState?
{
id: 1,
merit: "good",
title: "Good Habit",
count: 4,
text: "Words to be hidden",
visible: false,
},
{
id: 2,
merit: "bad",
title: "Bad Habit",
count: 1,
text: "Words to be hidden",
visible: false,
},
{
id: 3,
merit: "good",
title: "Good Habit",
count: 6,
text: "Words to be hidden",
visible: true,
},
]);
As you have it:
{
id: 1,
merit: "good",
title: "Good Habit",
count: 4,
text: "Words to be hidden",
visible: false,
},
{
id: 2,
merit: "bad",
title: "Bad Habit",
count: 1,
text: "Words to be hidden",
visible: false,
},
{
id: 3,
merit: "good",
title: "Good Habit",
count: 6,
text: "Words to be hidden",
visible: true,
},
];
habits.map()
-->Uncaught TypeError: habits.map is not a function```
Related
Today i want to create something like form in react native that looks like
It's pretty simple. i used this lib for radio button. However i want to change this text in the button when i click button next. I used following code.
import React, { useState, useRef } from "react";
import { StyleSheet, View, Button } from "react-native";
import RadioButtonRN from "radio-buttons-react-native";
export default function App() {
const numRef = useRef(0);
const questions = [
{
question: "What is localhost's IP address?",
answers: [
{ id: "1", text: "192.168.1.1" },
{ id: "2", text: "127.0.0.1", correct: true },
{ id: "3", text: "209.85.231.104" },
{ id: "4", text: "66.220.149.25" },
],
},
{
question: "What kind of11 fruit was used to name a computer in 1984?",
answers: [
{ id: "1", text: "Blackberry" },
{ id: "2", text: "Blueberry" },
{ id: "3", text: "Pear" },
{ id: "4", text: "Apple", correct: true },
],
},
];
return (
<View>
<RadioButtonRN
data={questions[numRef.current].answers.map((item) => ({
label: item.text,
correct: item.correct,
}))}
selectedBtn={(e) => {
console.log(e);
}}
/>
<Button
title="Next"
onPress={() => {
numRef.current = numRef.current + 1;
}}
/>
</View>
);
}
So right now when i clicked on the next button, the only thing thats updated is variable
numRef
But questions[numRef.current] doesn't update text in the button.
How can i fix that?
Changing the value of a ref doesn't result in a re-render. For data that, when it gets changed, should result in a re-render, you should use state instead.
export default function App() {
const [num, setNum] = useState(0);
// ...
data={questions[num].answers.map((item) => ({
// ...
onPress={() => {
setNum(num + 1)l
}}
I am dynamically adding and removing Dropdowns based on the index of the array. I am setting the index using. When adding or removing a dropdown, I increment and decrement that index. The goal would be an array that looks something like this:
[
{ index: 0, type: "facebook" },
{ index: 1, type: "instagram" }
]
The problem is when I add a new dropdown, the index is incrementing by 2 or 3 instead of 1, resulting in the following output.
[
{ index: 0, type: "facebook" },
{ index: 2, type: "instagram" }
]
Here is the code for my component:
class SocialProfileComponent extends Component {
constructor(props) {
super(props);
let index = 0;
this.state = {
options: [
{value: 'email', label: 'Email'},
{value: 'instagram', label: 'Instagram'},
{value: 'linkedin', label: 'Linkedin'},
{value: 'pinterest', label: 'Pinterest'},
{value: 'skype', label: 'Skype'},
{value: 'tiktok', label: 'TikTok'},
{value: 'tumblr', label: 'Tumblr'},
{value: 'twitter', label: 'Twitter'},
{value: 'whatsapp', label: 'WhatsApp'},
{value: 'wordpress', label: 'Wordpress'},
{value: 'youtube', label: 'Youtube'},
{value: 'other', label: 'Other'},
],
socialDetails: [
{
index: index,
type: "",
link: "",
}
]
};
}
addNewRow = (e) => {
this.setState(prevState => ({
socialDetails: [
...prevState.socialDetails,
{
index: index++,
type: "",
link: "",
}
]
}));
}
deleteRow = (index) => {
this.setState({
socialDetails: this.state.socialDetails.filter(
(s) => index !== s.index
)
})
}
clickOnDelete(record) {
this.setState({
socialDetails: this.state.socialDetails.filter(r => r!==record)
})
}
componentDidMount() {
}
render = () => {
let {socialDetails, options} = this.state;
const {onInputChange} = this.props;
return (
<>
<SocialProfile
label={`Social profiles`}
addRow={this.addNewRow}
deleteRow={this.deleteRow}
socialDetails={socialDetails}
options={options}
onInputChange={onInputChange}
formKey={'socialprofiles'}
createKeyValue={this.createNewKeyValuePair}
placeholder={'Select'}
/>
</>
)
}
}
Code Sandbox
The behavior you are experiencing where the index increases by 2 or 3 is a result of React strict mode. In addition to other things, strict mode helps detect unexpected side effects to help you prepare your app for the upcoming concurrent mode feature. In concurrent mode, React will break up rendering into smaller chunks pausing and resuming work as necessary. This means that render phase lifecycle methods can be run more than once.
To help you prepare for this upcoming behavior of concurrent mode, strict mode will intentionally invoke render phase lifecycles twice to identify potential side effects. State updater functions are one instance of this, meaning that calling index++ in your state updater function will be run twice in strict mode.
The easiest solution would be to simply assign the new index to a variable before calling this.setState so that your state updater function is idempotent and can be called more than once.
addNewRow = (e) => {
const newIndex = ++this.index
this.setState((prevState) => ({
socialDetails: [
...prevState.socialDetails,
{
index: newIndex,
type: "",
link: ""
}
]
}));
};
i have the data structure like below,
const item = {
id: '1',
orders: [
{
id: '1',
title: 'order-1',
status: 'new',
startDate: '2020-08-13T00:00:00.000Z',
}
],
subItems: [
{
id: '1',
title: 'subitem-one',
status: 'new',
startDate: '2020-08-13T00:00:00.000Z',
orders: [
{
id: '1',
title: 'subitem1-order-one',
status: 'new',
},
{
id: '2',
title: 'subitem1-order-two',
status: 'new',
},
]
},
{
id: '2',
title: 'subitem-two',
status: 'new',
startDate: '2020-08-13T00:00:00.000Z',
orders: [
{
id: '2',
title: 'subitem2-order-one',
status: 'new',
},
},
I have to display each subitem name from above data in each card in a list.
below is how it should look in a list.
below is my code,
function Parent({items}: Props) {
//items is the same data as mentioned above
return (
//how should i map the items data to loop through each subitem and display it as in the picture above.
);
}
I am not sure how to loop through each subitem and display its name in a card using javascript and react.
could someone help me with this. thanks.
Ciao, assuming that you are using Card component from material ui you could do something like:
import { Card, CardHeader} from '#material-ui/core';
function Parent({items}: Props) {
//items is the same data as mentioned above
return (
items.subItems.map(el => {
<Card>
<CardHeader
title={el.title}
/>
</Card>
})
);
}
I'm assuming that Parent is a React functional component, so you can return the data in this way,
function Parent({items} : Props) {
return (
<div>
{
items.subItems.map(function (subItem) {
return <div>{subItem.title}</div>;
});
}
</div>
)
}
The template is shown below:
const Parent = () => {
const data = [...] // the data must be an array
return data.map(child => <Child data={child} />)
}
I am trying to create the left hand menu like this image for my test application using react, and I am getting the following warning/error at runtime on the browser console when loaded.
index.js:1375 Warning: Each child in a list should have a unique "key" prop.
Check the render method of `LeftNav`. See fb.me/react-warning-keys for more information.
in Fragment (at LeftNav.js:12)
in LeftNav (at App.js:131)
in div (at App.js:120)
in div (at App.js:118)
in div (at App.js:116)
in Router (created by BrowserRouter)
in BrowserRouter (at App.js:115)
in App (at src/index.js:7)
Each of the navItems props items have distinct IDs, so every component should already have unique keys?
Should the React.Fragment have a unique key as well?
The LeftNav component is as below
class LeftNav extends Component {
render() {
return (
<ul>
{this.props.navItems.map((navItem) => {
return (
<React.Fragment>
<LeftNavItem key={navItem.id} navItem={navItem} menuLevel={0} />
{navItem.subMenu.map((subMenuItem) => {
return <LeftNavItem key={subMenuItem.id} navItem={subMenuItem} menuLevel={1} />
})}
</React.Fragment>
)
})}
</ul>
)
}
}
LeftNav.propTypes = {
navItems: PropTypes.array.isRequired
}
export default LeftNav;
LeftNavItem component is as below.
import React, { Component } from 'react'
import PropTypes from 'prop-types';
class LeftNavItem extends Component {
getClassName = () => {
if(this.props.menuLevel === 0) {
return 'parent_wrapper';
} else {
return '';
}
}
render() {
return (
<li className={this.getClassName()}>
{this.props.navItem.title}
</li>
)
}
}
LeftNavItem.propTypes = {
navItem: PropTypes.object.isRequired
}
export default LeftNavItem;
The props for LeftNav class is
leftNav: [
{
id: 1,
title: 'System Admin',
active: false,
subMenu: [
{
id: 2,
title: '',
active: false
},
{
id: 3,
title: '',
active: false
},
{
id: 4,
title: '',
active: false
},
{
id: 5,
title: '',
active: false
},
{
id: 6,
title: '',
active: false
},
{
id: 7,
title: '',
active: false
},
{
id: 8,
title: '',
active: false
},
{
id: 9,
title: '',
active: false
}
]
},
{
id: 10,
title: 'Reports',
active: false,
subMenu: [
{
id: 11,
title: 'Reports',
active: false
},
{
id: 12,
title: 'Dashboard',
active: true
},
{
id: 13,
title: 'Templates',
active: false
}
]
Thanks!!
Add your key to the fragment instead of the nested tag.
return (
<React.Fragment key={navItem.id}>
<LeftNavItem navItem={navItem} menuLevel={0} />
{navItem.subMenu.map((subMenuItem) => {
return <LeftNavItem key={subMenuItem.id} navItem={subMenuItem} menuLevel={1} />
})}
</React.Fragment>
)
I want to add sub categories with react on a list, but when i click on sub cat, i have two events : First on sub and second on parent category.
How can i have only child category ?
There is my actual code :
getList(myList){
return myList.map((item) => {
let subList = '';
if (item.hasSub){
subList = (<ul>{this.getList(item.sub)}</ul>)
}
return (
<li onClick={() => {this.props.clicHandler(item.id)}}>{item.title}<div>{subList}</div></li>);
})
}
I'm using recursive method to create my list.
My actual array is like this :
this.list.push({id: 1, title:'coucou' , hasSub: false});
this.list.push({id: 2, title:'toto' , hasSub: false});
this.list.push({id: 3, title: 'cat', sub: [{id:4, title:'titi' , hasSub: false}, {id:5, title:'tutu' , hasSub: false}] , hasSub: true});
Thank you for your help !
The problem is that click events "bubble" up to parent elements. The solution is to call Event.prototype.stopPropagation on it.
In the below snippet I've added a clickHandler method to the component, and in getList I pass both the event (evt) and item.id to it. In that method I call evt.stopPropagation() before calling this.props.clickHandler. However, you could also do this in the parent component's clickHandler method, or within getList. Choose whatever best suits your use case.
class App extends React.Component {
constructor() {
super();
this.clickHandler = this.clickHandler.bind(this);
}
render() {
return <ul>{this.getList(this.props.items)}</ul>;
}
getList(list) {
return list.map((item) => {
const subList = item.hasSub && (<ul>{this.getList(item.sub)}</ul>);
return (
<li key={item.id} onClick={evt => {this.clickHandler(evt, item.id)}}>{item.title}<div>{subList}</div></li>
);
});
}
clickHandler(evt, itemId) {
// Stop event propagatation before calling handler passed in props
evt.stopPropagation();
this.props.clickHandler(itemId);
}
}
const data = [
{ id: 'a', title: 'A', hasSub: false },
{ id: 'b', title: 'B', hasSub: true, sub: [
{ id: 'b1', title: 'B1', hasSub: true, sub: [
{ id: 'b1a', title: 'B1a', hasSub: false },
] },
{ id: 'b2', title: 'B2', hasSub: false },
] },
{ id: 'c', title: 'C', hasSub: true, sub: [
{ id: 'c1', title: 'C1', hasSub: false },
{ id: 'c2', title: 'C2', hasSub: false },
] },
];
function clickHandler(itemId) {
console.log('clicked item', itemId);
}
ReactDOM.render(<App items={data} clickHandler={clickHandler}/>, document.querySelector('div'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.js"></script>
<div></div>
P.S. Your code is excellent, but might I recommend a somewhat more "React way" to render nested items?
What your component's render method does is call a getItems function that in turn calls itself, recursively. However, if you recall that most React components can themselves be thought of as functions, you can refactor your code so that your render method instead renders an <Items> component that recursively renders instances of itself.
This approach tends to yield smaller, functional components that are more reusable and easier to test. Take a look in the snippet below.
const Items = ({items, onClick}) => (
<ul>
{items.map(item => (
<li key={item.id} onClick={evt => onClick(evt, item.id)}>
{item.title}
{item.hasSub && <Items items={item.sub} onClick={onClick}/>}
</li>
))}
</ul>
);
const data = [
{ id: 'a', title: 'A', hasSub: false },
{ id: 'b', title: 'B', hasSub: true, sub: [
{ id: 'b1', title: 'B1', hasSub: true, sub: [
{ id: 'b1a', title: 'B1a', hasSub: false },
] },
] },
];
function clickHandler(evt, itemId) {
evt.stopPropagation();
console.log('clicked item', itemId);
}
ReactDOM.render(<Items items={data} onClick={clickHandler}/>, document.querySelector('div'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.js"></script>
<div></div>
You need to prevent the event propagation :
getList(myList){
return myList.map((item) => {
let subList = '';
if (item.hasSub){
subList = (<ul>{this.getList(item.sub)}</ul>)
}
return (
<li onClick={(event) => {event.preventDefault(); this.props.clicHandler(item.id)}}>{item.title}<div>{subList}</div></li>);
})
}
This way the first item to catch the click will stop the click propagation.