Two way communication between two functional React JS components - javascript

I have two functional components built in React, one is an Item component - it holds some data about stuff, with optional graphics, some text data and price information. On the bottom there is a button, that allows you to select this particular item. It also keeps information in its props on ID of currently selected Item - that's how i planned to solve this problem.
My second component is a ItemList - it basically holds a list of aforemetioned Items - plus it sorts all the items and must keep information about which component is currently selected - the selected one basically looks different - some stuff like the border box and button's colour gets switched via CSS.
My logic to implement goes like this - when user clicks on a "Select" button of a particular Item, the Item should change its look (unless it's already selected, then do nothing), after that somehow propagate info up onto the ItemList, so that it can "disable" the previously selected component. There can be only one selected Item, and once user decide to select another one, the previously selected should change its state and go back to unselected standard graphic style.
I've ran across a solution with state in the ItemList component plus passing a function via props into Item, but that doesn't solve the second part - ItemList needs to get info about a change, so it can rerender all the components according to actual state. What part of React API should I dive into to solve this issue?
Here is code for my components:
Item
interface Props {
receivedObject: itemToDisplay;
selectedItemId: string;
onClick?: () => void;
}
export default function Item(props: Props) {
const {name, description, price} = props.receivedObject;
const imageUrl = props.receivedObject?.media?.mainImage?.small?.url;
const priceComponent = <Price price={price}/>;
const [isItemSelected, setSelection] = useState(props.selectedItemId == props.receivedObject.id);
const onClick = props.onClick || (() => {
setSelection(!isItemSelected)
});
return (
<>
<div className="theDataHolderContainer">
// displayed stuff goes here
<div className="pickButtonContainer">
// that's the button which should somehow send info "upwards" about the new selected item
<Button outline={isItemSelected} color="danger" onClick={onClick}>{isItemSelected ? "SELECTED" : "SELECT"}</Button>
</div>
</div>
</>)
};
ItemList
interface Props {
packageItems: Array<itemToDisplay>
}
export default function ItemList(props: Props) {
const itemsToDisplay = props.packageItems;
itemsToDisplay.sort((a, b) =>
a.price.finalPrice - b.price.finalPrice
);
let selectedItemId = itemsToDisplay[0].id;
const [currentlySelectedItem, changeCurrentlySelectedItem] = useState(selectedItemId);
const setSelectedItemFunc = () => {
/* this function should be passed down as a prop, however it can only
* have one `this` reference, meaning that `this` will refer to singular `Item`
* how do I make it change state in the `ItemList` component?
*/
console.log('function defined in list');
};
return(
<div className="packageNameList">
<Item
key={itemsToDisplay[0].id}
receivedObject={itemsToDisplay[0]}
onClick={setSelectedItemFunc}
/>
{itemsToDisplay.slice(1).map((item) => (
<Item
key={item.id}
receivedObject={item}
onClick={setSelectedItemFunc}
/>
))}
</div>
);
}

In React, the data flows down, so you'd better hold state data in a stateful component that renders presentation components.
function ListItem({ description, price, selected, select }) {
return (
<li className={"ListItem" + (selected ? " selected" : "")}>
<span>{description}</span>
<span>${price}</span>
<button onClick={select}>{selected ? "Selected" : "Select"}</button>
</li>
);
}
function List({ children }) {
return <ul className="List">{children}</ul>;
}
function Content({ items }) {
const [selectedId, setSelectedId] = React.useState("");
const createClickHandler = React.useCallback(
id => () => setSelectedId(id),
[]
);
return (
<List>
{items
.sort(({ price: a }, { price: b }) => a - b)
.map(item => (
<ListItem
key={item.id}
{...item}
selected={item.id === selectedId}
select={createClickHandler(item.id)}
/>
))}
</List>
);
}
function App() {
const items = [
{ id: 1, description: "#1 Description", price: 17 },
{ id: 2, description: "#2 Description", price: 13 },
{ id: 3, description: "#3 Description", price: 19 }
];
return (
<div className="App">
<Content items={items} />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
.App {
font-family: sans-serif;
}
.List > .ListItem {
margin: 5px;
}
.ListItem {
padding: 10px;
}
.ListItem > * {
margin: 0 5px;
}
.ListItem:hover {
background-color: lightgray;
}
.ListItem.selected {
background-color: darkgray;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

Related

React how to render only parts of an array?

I am still a bit new to react and how it works. I have a projects.js file with a list of objects that look like this:
id: 0,
name: "Placeholder Project 1",
description: "Project description",
image: "./assets/images/placeholder-01.png"
There are 6 objects in this array. I am trying to render only 3 project objects at a time, and then include a button that will "load more" projects later. However, I am having trouble with just the rendering part. My component looks like this:
import React, { Component } from "react";
import NavTab from "./NavTab";
import { Card } from "react-bootstrap";
import { PROJECTS } from "../shared/projects";
function RenderProject({projects, projectsDisplayArray}) {
const tempArray = projectsDisplayArray;
return(
<div className="row">
{
projects.map(project => {
tempArray.indexOf(project) > -1 ? console.log('in array already') : tempArray.push(project)
console.log(tempArray.length)
if (tempArray.length >= 3){
console.log('in the if')
return (
<Card key={project.id}>
<Card.Img variant="top" src={project.image} />
<Card.Body>
<Card.Title>{project.name}</Card.Title>
<Card.Text>
{project.description}
</Card.Text>
</Card.Body>
<button className="btn align-self-center">Go somewhere</button>
</Card>
)
}
else {
return(<div>Else return div</div>)
}
})
}
</div>
)
}
export default class Projects extends Component {
constructor(props){
super(props);
this.state = {
projectsPerScreen: 3,
currentPage: 0,
projects: PROJECTS,
projectsDisplayArray: []
}
}
modifyProjectsDisplayArray = props => {
this.setState({projectsDisplayArray: [...this.state.projectsDisplayArray, props]})
}
render() {
let i = 0;
return(
<React.Fragment>
<NavTab/>
<div className="projects">
<div className="container">
<button type="button" className="btn">Click</button>
<h1>Projects: </h1>
<RenderProject projects={this.state.projects} projectsDisplayArray={this.state.projectsDisplayArray} />
<button type="button" className="btn" onClick={() => console.log(this.state.projectsDisplayArray)}>console log</button>
</div>
</div>
</React.Fragment>
)
}
}
I am very confused on how the return method for RenderProject is working. When I begin the mapping process, I want to add each project to an array so I can keep track of how many and what projects are being rendered. When the array length hits three, I want it to stop rendering. But whenever I do this, my line if (tempArray.length <= 3) behaves in a way I don't expect it to. With how it is now, it won't return the <Card> and will instead return the else <div> for all 6 objects. But if I change the if statement to be if (tempArray.length >= 3) it will render all 6 objects inside of the array and no else <div>s. What should I be doing instead?
You can use state to keep track of how many array items to render by keeping track of the index you want to slice your array at. Then use a new sliced array to map over. Then you can have a button that increases the index state by 3 every time it is clicked:
import { useState } from 'react';
const Component = () => {
const [index, setIndex] = useState(3)
const yourArray = [...]
const itemsToRender = yourArray.slice(0, index);
return (
<>
<button onClick={() => setIndex(index + 3)}>load more</button>
<ul>
{ itemsToRender.map((item) => <li>{item}</li>) }
</ul>
</>
);
}
I would try something like that, using the index parameter (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map). For sake of simplicity I will only add the part with the mapping function.
<div className="row">
{
projects.map((project, index) => {
if (index < 3){
return (
<Card key={project.id}>
<Card.Img variant="top" src={project.image} />
<Card.Body>
<Card.Title>{project.name}</Card.Title>
<Card.Text>
{project.description}
</Card.Text>
</Card.Body>
<button className="btn align-self-center">Go somewhere</button>
</Card>
)
}
else {
return (<div>Else return div</div>);
}
})
}
</div>
I would however consider using filter (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) or anyway create a subarray with the needed elements, then map over that array.
The important thing to remember with React is that you don't add things to the DOM in the traditional way - you update state. So, in this contrived example, I've added some data to state, and I also have a count state too.
count is initially set to 1 and on the first render the getItems function returns a new array of three items which you can then map over to get your JSX.
When you click the button it calls handleClick which sets a new count state. A new render happens, and a new set of count * 3 data is returned (six items).
And so on. I also added a condition that when the count is greater than the number of items in your data the button gets disabled.
const { useState } = React;
// So here I'm just passing in some data
function Example({ data }) {
// Initialise the items state (from the data passed
// in as a prop - in this example), and a count state
const [ items, setItems ] = useState(data);
const [ count, setCount] = useState(1);
// Returns a new array of items determined by the count state
function getItems() {
return [ ...items.slice(0, count * 3)];
}
// Sets a new count state when the button is clicked
function handleClick() {
setCount(count + 1);
}
// Determines whether to disable the button
function isDisabled() {
return count * 3 >= items.length;
}
return (
<div>
{getItems().map(item => {
return <div>{item}</div>;
})}
<button
onClick={handleClick}
disabled={isDisabled()}
>Load more
</button>
</div>
);
};
// Our data. It gets passed into the component
// as a property which then gets loaded into state
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
ReactDOM.render(
<Example data={data} />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

ReactJs: How to pass data from one component to another?

If two or more cards from a component are selected how to pass data to a button component?
I have a landing page that holds two components; Template list and a button.
<TemplateList templates={templates} />
<MenuButton style={actionButton} onClick={onOnboardingComplete}>
Select at least 2 options
</MenuButton>
The TemplateList brings in an array of information for my Template and creates Template Cards with it. I am then able to select my cards and unselect them.
My Button when pressed just takes me to the next step of the onboarding.
I would like to know how I should approach linking these two components. My button now is gray and would like this to happen:
1. Button is gray and user cant go on to next step, until selecting two or more cards.
2. When two or more cards are selected, button turn blue and they are able to press the button to continue.
This is my Template List:
export type Template = {
title: string;
description: string;
imgURL: string;
id?: number;
};
type Props = {
templates: Template[];
};
const TemplateList = ({ templates }: Props) => {
return (
<div className={styles.scrollContainer}>
{templates.map((item) => (
<TemplateCard
title={item.title}
description={item.description}
img={item.imgURL}
classNameToAdd={styles.cardContainer}
key={item.id}
/>
))}
</div>
);
};
export default TemplateList;
And this my Template Card:
type Props = {
title: string;
description: string;
img: string;
classNameToAdd?: string;
classNameOnSelected?: string;
};
const TemplateCard = ({ title, description, img, classNameToAdd, classNameOnSelected }: Props) => {
const { aspectRatio, vmin } = useWindowResponsiveValues();
let className = `${styles.card} ${classNameToAdd}`;
const [selected, setSelected] = useState(false);
const handleClick = () => {
setSelected(!selected);
};
if (selected) {
className += `${styles.card} ${classNameToAdd} ${classNameOnSelected}`;
}
return (
<div style={card} className={className} onClick={handleClick}>
<img style={imageSize} src={img}></img>
<div style={cardTitle}>
{title}
{selected ? <BlueCheckIcon style={blueCheck} className={styles.blueCheck} /> : null}
</div>
<div style={descriptionCard}>{description}</div>
</div>
);
};
TemplateCard.defaultProps = {
classNameOnSelected: styles.selected,
};
export default TemplateCard;
This is how it looks like now.
There are at least 3 ways to implement your requirement:
OPTION 1
Put Button component inside the <TemplateList> component
Add one useState tied to <TemplateList> component to hold the number of
selected cards
Add two new props onSelectCard and onDeselectCard to <TemplateCard> to increment/decrement newly created state by 1 for each selected/deselected item
Implement callback functions inside <TemplateList component (code below)
Call onSelectCard inside <TemplateCard> when needed (code below)
TemplateList Component
const TemplateList = ({ templates }: Props) => {
const [noOfSelectedCards, setNoOfSelectedCards] = useState(0);
handleSelect = () => setNoOfSelectedCards(noOfSelectedCards + 1);
handleDeselect = () => setNoOfSelectedCards(noOfSelectedCards - 1);
return (
<div classNae="template-list-container">
<div className={styles.scrollContainer}>
{templates.map((item) => (
<TemplateCard
title={item.title}
description={item.description}
img={item.imgURL}
classNameToAdd={styles.cardContainer}
key={item.id}
onSelectCard={handleSelect}
onDeselectCard={handleDeselect}
/>
))}
</div>
<MenuButton style={actionButton} onClick={onOnboardingComplete} className={noOfSelectedCards === 2 ? 'active' : ''}>
Select at least 2 options
</MenuButton>
</div>
);
};
TemplateCard Component
const TemplateCard = ({ ..., onSelectCard, onDeselectCard }: Props) => {
...
const [selected, setSelected] = useState(false);
const handleClick = () => {
if(selected) {
onDeselectCard();
} else {
onSelectCard();
}
setSelected(!selected);
};
...
};
Now, you'll have the current number of selected cards in your state noOfSelectedCards (<TemplateList> component) so you can conditionally render whatever className you want for your button or do something else with it.
OPTION 2
Using React Context to share state between components
It's okay to use React Context for such cases, but if your requirements contains other similar cases for handling/sharing states between components across the app, I suggest you take a look at the 3rd option.
OPTION 3
Using State Management like Redux to handle global/shared states between components.
This is probably the best option for projects where sharing states across the app is quite common and important. You'll need some time to understand concepts around Redux, but after you do that I assure you that you'll enjoy working with it.

React - How to use a key to setState on a specific object in an array when modifying a property?

I have created a menu card that will show a card for each object in an array. The object holds details about that menu item. I am looking to affect one property of that menu item but I am unsure how to potentially use the key or an id to affect only that item. How can I dynamically select an object from an array based on its key or some other unique identifier?
Right now as a stand-in I use handleCategory to affect the first object in the array. I am looking instead to affect the object based on which Item card I am using.
Below is what my code looks like (basic example):
Home.js
import React, { Component } from 'react';
import { Form, Button } from 'react-bootstrap';
import Items from './items'
import Navbar from './navbar'
class Home extends Component {
constructor() {
super();
this.state = {
value: 'enter',
condimentCount: 5,
menuItems: [
{
"id": 1234,
"item_name": 'chow mein',
"category_id": 'meal'
},
{
"id": 1235,
"item_name": '',
"category_id": 'meal'
}
]
};
this.handleCategory = this.handleCategory.bind(this);
}
handleCategory = (event, id) => {
console.log('changing meal type', event.target.value)
//this.setState({value: event.target.value});
// 1. Make a shallow copy of the items
let items = [...this.state.menuItems];
// 2. Make a shallow copy of the item you want to mutate
let item = {...items[0]};
// 3. Replace the property you're intested in
item.category_id = event.target.value;
// 4. Put it back into our array. N.B. we *are* mutating the array here, but that's why we made a copy first
items[0] = item;
// 5. Set the state to our new copy
this.setState({menuItems: items});
}
render() {
let menuItems = null;
if (this.state.menuItems) {
menuItems = (
<div>
{
this.state.menuItems.map((item, i) => {
return <div key={i}>
<MenuItem
key={item.id} //IS THIS CORRECT?
item_name={item.item_name}
category_id={item.category_id}
handleCategory={this.handleCategory}
/>
</div>
})
}
</div>
)
}
return (
<div>
<Navbar />
<div>
{Items}
</div>
</div>
);
}
}
export default Home;
Items.js
import React, { Component } from 'react';
import classes from './items.module.css'
const Items = (props) => {
return (
<div className={classes.cards}>
<form>
<label>
What kind of meal item is this (e.g. meal, drink, side):
<select value={props.category_id} onChange={props.handleCategory}>
<option value="meal">meal</option>
<option value="drink">drink</option>
<option value="side">side</option>
</select>
</label>
</form
</div>
)
}
export default Items;
I'm not really sure what you're trying to do in handleCategory, but I understand your question I believe.
You can use the index from map to access the menu items in handleCategory.
this.state.menuItems.map((item, i) => {
return <div key={i}>
<MenuItem
item_name={item.item_name}
category_id={item.category_id}
handleCategory={e => this.handleCategory(e, i)}
/>
</div>
})

React Hooks populate array

Scenario: I have an array of objects to populate a list. When the user clicks on one of the items, that item needs to be come "selected" visually and what I would think is pushed to a new array called "selectedList". If the user clicks on another unselected item that becomes selected and also maintaining the previous selection. If the user clicks on a selected item it needs to toggle the selected state to unselected and remove within the selectedList array.
My thinking on this is to have a selectedList array which consists of the default object added first, then as the user clicks on other objects, push them into the array. I have this so far.
How can I toggle the selection within the array and how can I check the selectedList array so I can tell the "FundCard" component to set it's selected state to true so it visually updates to a selected state?
Here's what I have so far:
import React, {useState, useEffect} from 'react';
import styled from 'styled-components';
import FundCard from '../FundCard'
const FundDLWrap = styled.ul`
margin: 0;
padding: 0;
list-style-type: none;
border-left: 1px solid #e6e6e6;
margin-top: -40px;
height: 1007px;
overflow-y: scroll;
li {
&:hover {
cursor: pointer;
}
}
`
const FundDetailList = ({data, fundID}) => {
const [selectedList, setSelectedList] = useState([fundID]);
const listSelection = (i) => {
setSelectedList([...selectedList, i]);
}
console.log("selected list", selectedList);
return (
<FundDLWrap>
{data.map((item, i) => {
// one item must always be selected - the item ID the user came from
const selected = fundID === i;
return (
<li key={i} onClick={() => listSelection(i)}>
<FundCard data={item} vertical={true} selected={selected} />
</li>
);
})}
</FundDLWrap>
)
}
export default FundDetailList;
here's what one of the data objects look like:
{
saved: false,
name: 'Title here',
dailyChange: "3.52",
inc: true,
price: '132.42',
priceDate: '11 Mar 2020',
volRating: 1,
},
please note the data is external and is not kept in state.
If the specified index i is in the selectedList then filter it out, otherwise, append it to a copy of the array.
Use the same selectedList.includes(i) check in the return to set the selected prop of FundCard.
const FundDetailList = ({data, fundID}) => {
// Don't populate index 0 with fundID, it'll get matched in the map function
const [selectedList, setSelectedList] = useState([]);
const listSelection = (i) => {
setSelectedList(
selectedList.includes(i)
? selectedList.filter(index => index !== i)
: [... selectedList, i]
);
}
return (
<FundDLWrap>
{data.map((item, i) => {
// one item must always be selected - the item ID the user came from
const selected = selectedList.includes(i) || fundID === i;
return (
<li key={i} onClick={() => listSelection(i)}>
<FundCard data={item} vertical selected={selected} />
</li>
);
})}
</FundDLWrap>
)
}

Toggle only the menu clicked in Reactjs

I am making a menu and submenus using recursion function and I am in the need of help to open only the respective menu and sub menu's..
For button and collapse Reactstrap has been used..
Recursive function that did menu population:
{this.state.menuItems &&
this.state.menuItems.map((item, index) => {
return (
<div key={item.id}>
<Button onClick={this.toggle.bind(this)}> {item.name} </Button>
<Collapse isOpen={this.state.isToggleOpen}>
{this.buildMenu(item.children)}
</Collapse>
</div>
);
})}
And the buildMenu function as follows,
buildMenu(items) {
return (
<ul>
{items &&
items.map(item => (
<li key={item.id}>
<div>
{this.state.isToggleOpen}
<Button onClick={this.toggle.bind(this)}> {item.name} </Button>
<Collapse isOpen={this.state.isToggleOpen}>
{item.children && item.children.length > 0
? this.buildMenu(item.children)
: null}
</Collapse>
</div>
</li>
))}
</ul>
);
}
There is no problem with the code as of now but I am in the need of help to make menu -> submenu -> submenu step by step open and closing respective levels.
Working example: https://codesandbox.io/s/reactstrap-accordion-9epsp
You can take a look at this example that when you click on any menu the whole level of menus gets opened instead of clicked one..
Requirement
If user clicked on menu One, then the submenu (children)
-> One-One
needs to get opened.
And then if user clicked on One-One,
-> One-One-One
-> One - one - two
-> One - one - three
needs to get opened.
Likewise it is nested so after click on any menu/ children their respective next level needs to get opened.
I am new in react and reactstrap way of design , So any help from expertise would be useful for me to proceed and learn how actually it needs to be done.
Instead of using one large component, consider splitting up your component into smaller once. This way you can add state to each menu item to toggle the underlying menu items.
If you want to reset al underlying menu items to their default closed position you should create a new component instance each time you open up a the underlying buttons. By having <MenuItemContainer key={timesOpened} the MenuItemContainer will be assigned a new key when you "open" the MenuItem. Assigning a new key will create a new component instance rather than updating the existing one.
For a detailed explanation I suggest reading You Probably Don't Need Derived State - Recommendation: Fully uncontrolled component with a key.
const loadMenu = () => Promise.resolve([{id:"1",name:"One",children:[{id:"1.1",name:"One - one",children:[{id:"1.1.1",name:"One - one - one"},{id:"1.1.2",name:"One - one - two"},{id:"1.1.3",name:"One - one - three"}]}]},{id:"2",name:"Two",children:[{id:"2.1",name:"Two - one"}]},{id:"3",name:"Three",children:[{id:"3.1",name:"Three - one",children:[{id:"3.1.1",name:"Three - one - one",children:[{id:"3.1.1.1",name:"Three - one - one - one",children:[{id:"3.1.1.1.1",name:"Three - one - one - one - one"}]}]}]}]},{id:"4",name:"Four"},{id:"5",name:"Five",children:[{id:"5.1",name:"Five - one"},{id:"5.2",name:"Five - two"},{id:"5.3",name:"Five - three"},{id:"5.4",name:"Five - four"}]},{id:"6",name:"Six"}]);
const {Component, Fragment} = React;
const {Button, Collapse} = Reactstrap;
class Menu extends Component {
constructor(props) {
super(props);
this.state = {menuItems: []};
}
render() {
const {menuItems} = this.state;
return <MenuItemContainer menuItems={menuItems} />;
}
componentDidMount() {
loadMenu().then(menuItems => this.setState({menuItems}));
}
}
class MenuItemContainer extends Component {
render() {
const {menuItems} = this.props;
if (!menuItems.length) return null;
return <ul>{menuItems.map(this.renderMenuItem)}</ul>;
}
renderMenuItem(menuItem) {
const {id} = menuItem;
return <li key={id}><MenuItem {...menuItem} /></li>;
}
}
MenuItemContainer.defaultProps = {menuItems: []};
class MenuItem extends Component {
constructor(props) {
super(props);
this.state = {isOpen: false, timesOpened: 0};
this.open = this.open.bind(this);
this.close = this.close.bind(this);
}
render() {
const {name, children} = this.props;
const {isOpen, timesOpened} = this.state;
return (
<Fragment>
<Button onClick={isOpen ? this.close : this.open}>{name}</Button>
<Collapse isOpen={isOpen}>
<MenuItemContainer key={timesOpened} menuItems={children} />
</Collapse>
</Fragment>
);
}
open() {
this.setState(({timesOpened}) => ({
isOpen: true,
timesOpened: timesOpened + 1,
}));
}
close() {
this.setState({isOpen: false});
}
}
ReactDOM.render(<Menu />, document.getElementById("root"));
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" />
<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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reactstrap/8.4.1/reactstrap.min.js"></script>
<div id="root"></div>
You will want to create an inner component to manage the state at each level.
For example, consider the following functional component (I'll leave it to you to convert to class component):
const MenuButton = ({ name, children }) => {
const [open, setOpen] = useState(false);
const toggle = useCallback(() => setOpen(o => !o), [setOpen]);
return (
<>
<Button onClick={toggle}>{name}</Button>
<Collapse open={open}>{children}</Collapse>
</>
);
};
This component will manage whether to display its children or not. Use it in place of all of your <div><Button/><Collapse/></div> sections, and it will manage the open state for each level.
Keep shared state up at the top, but if you don't need to know whether something is expanded for other logic, keep it localized.
Also, if you do need that info in your parent component, use the predefined object you already have and add an 'open' field to it which defaults to false. Upon clicking, setState on that object to correctly mark the appropriate object to have the parameter of true on open.
Localized state is much cleaner though.
Expanded Example
import React, { Component, useState, useCallback, Fragment } from "react";
import { Collapse, Button } from "reactstrap";
import { loadMenu } from "./service";
const MenuButton = ({ name, children }) => {
const [open, setOpen] = React.useState(false);
const toggle = useCallback(() => setOpen(o => !o), [setOpen]);
return (
<Fragment>
<Button onClick={toggle}>{name}</Button>
<Collapse open={open}>{children}</Collapse>
</Fragment>
);
};
class Hello extends Component {
constructor(props) {
super(props);
this.state = {
currentSelection: "",
menuItems: [],
};
}
componentDidMount() {
loadMenu().then(items => this.setState({ menuItems: items }));
}
buildMenu(items) {
return (
<ul>
{items &&
items.map(item => (
<li key={item.id}>
<MenuButton name={item.name}>
{item.children && item.children.length > 0
? this.buildMenu(item.children)
: null}
</MenuButton>
</li>
))}
</ul>
);
}
render() {
return (
<div>
<h2>Click any of the below option</h2>
{this.state.menuItems &&
this.state.menuItems.map((item, index) => {
return (
<MenuButton name={item.name}>
{this.buildMenu(item.children)}
</MenuButton>
);
})}
</div>
);
}
}
export default Hello;

Categories

Resources