Handling state of multiple instances of same component - reactjs

I have this Item component that I am using in another component:
import React, { useState } from "react";
import Button from "./Button";
const Item = ({ name }) => {
const [isSelected, setIsSelected] = useState(false);
const toggle = () => {
setIsSelected(!isSelected);
};
var buttonColor;
var buttonText;
if (isSelected === true) {
buttonColor = "bg-button-blue";
buttonText = "Selected";
} else {
buttonColor = "bg-button-gray";
buttonText = "Select";
}
return (
<div onClick={toggle} className="flex ml-2 items-center">
<div className="text-misc-gray text-xs w-40">{name}</div>
<div>
<Button
text={buttonText}
color={buttonColor}
height={"h-8"}
width={"w-18"}
></Button>
</div>
</div>
);
};
export default Item;
In the other component, I have multiple instances of this Item component, representing different items. The Item component can with a click change property, like text and color for the button.
The problem is that in the other component, multiple of these Items can be toggled at the same time.
I would like that out of every instance of the Item component in the other component, only a single one can be toggled on at the same time. So if I select one item, the previous (if any selected) will be "unselected", changing the text and color back to the original state.
Can this be solved by only making changes in the Item component, or do I also need to make changes where it's being imported?

Can this be solved by only making changes in the Item component
Isolating state is good, but in this situation, Item state has dependencies on other components, so we cannot isolate that state completely.
I'd suggest that you should lift your state isSelected up to the parent component, and pass that state down to each Item for UI update.
import React, { useState } from "react";
const ParentComponent = () => {
const [selectedIndex, setSelectedIndex] = useState();
//[...Array(5)] is representing your actual items
return (
<div>
[...Array(5)].map((value, index) => <Item key={index} isSelected={selectedIndex === index} index={index} toggle={(i) => setSelectedIndex(i)} />)
</div>
);
};
And then change Item props with a little logic modification
import React, { useState } from "react";
import Button from "./Button";
const Item = ({ name, isSelected, index, toggle }) => {
var buttonColor;
var buttonText;
if (isSelected === true) {
buttonColor = "bg-button-blue";
buttonText = "Selected";
} else {
buttonColor = "bg-button-gray";
buttonText = "Select";
}
return (
<div onClick={() => toggle(index)} className="flex ml-2 items-center">
<div className="text-misc-gray text-xs w-40">{name}</div>
<div>
<Button
text={buttonText}
color={buttonColor}
height={"h-8"}
width={"w-18"}
></Button>
</div>
</div>
);
};
export default Item;

Can this be solved by only making changes in the Item component
No good way using the React paradigm, because you want one instance to affect another instance, where the other instance is not a child.
In the ancestor component of the <Item>s, create a state variable that holds the index (or ID, or name, or some other uniquely identifying aspect) of the Item component currently toggled on, and then pass down an isSelected prop to each Item, as well as the state setter. (The individual Items should no longer have an isSelected state.) Perhaps something along the lines of:
const numItems = 5;
const Parent = () => {
const [selectedIndex, setSelectedIndex] = useState(-1);
const makeToggle = (i) => () => {
setSelectedIndex(i === selectedIndex ? -1 : i);
};
return (
<div>
{ Array.from(
{ length: numItems },
(_, i) => <Item
isSelected={selectedIndex == i}
toggle={makeToggle(i)}
/>
)}
</div>
);
};

Related

How set state for one individual component React when having multiple components

I have this Navbar with 3 tabs, and I managed to build a hook that sets a different style when clicked (changing its class); however, i don't know how target a state directly to just one tab. When clicked, all of then change their states. how I use the "this" in react in a case like this
const [isActive, setIsActive] = useState(false);
const handleClick = () => {
setIsActive(current => !current);
};
const setName = () => {
return (isActive ? 'true' : 'false');
}
return (
<NavStyled>
<div className="navbar-div">
<nav className="nav">
<p className={setName()} onClick={handleClick} >Basic</p>
<p className={setName()} onClick={handleClick} >Social</p>
<p className={setName()} onClick={handleClick} >Certificates</p>
</nav>
</div>
</NavStyled>
);
};
export default Navbar; ```
Navbar is a function component, not a hook.
You need to store either the currentTabIndex or currentTabName in the state.
var [currentTabName, setCurrentTabName] = useState('Basic');
handleClick=(evt)=> {
setCurrentName(evt.target.textContent);
};
['Basic','Social','Certificates'].map((tabName, i)=> {
let clazz = (tabName == currentTabName)?'active':'';
return <p key={tabName} className={clazz} onClick={handleClick} >{tabName}</p>
});

className statement not re-rending when useState is set

When I click a "Thread"/button handleClick get's called and the currentIndex is updated/set which triggers the button's className ternary statement to execute as true. However, as there are many <li> elements, the other elements do not re-render className to the false statement. currentIndex should not == thread.id for those elements.
ThreadItem component
import { useState } from 'react'
const ThreadItem = ({ thread, changeThread }) => {
const [currentIndex, setCurrentIndex] = useState('')
// On thread click
const handleClick = (threadId) => {
setCurrentIndex(threadId)
}
return (
<li className="chat-item">
<form onSubmit={changeThread}>
<button className={currentIndex == thread.id ? 'side-menu-links-focus threads' : 'side-menu-links threads'} onClick={() => handleClick(thread.id)}>{`#${thread.threadType}`}</button>
</form>
</li>
)
}
export default ThreadItem
Parent ThreadList component
import ThreadItem from './ThreadItem'
const ThreadList = ({ threads, changeThread }) => {
return (
<ul className='chat-list'>
{threads.map((thread) => (
<ThreadItem
key={thread.id}
thread={thread}
changeThread={changeThread}
/>
))}
</ul>
)
}
export default ThreadList
If I understand your problem correctly, you want to click a button, and have a single ThreadItem to change className.
The problem I see is that you have added useState to every ThreadItem. Instead, you should move the state above to the ThreadList component.
This way, there is only one instance of state for the entire list of items. Simply pass the active prop and an onClick handler to each ThreadItem.
ThreadList
import React, { useState } from 'react';
import ThreadItem from './ThreadItem';
const ThreadList = ({ threads, changeThread }) => {
const [currentIndex, setCurrentIndex] = useState('');
// On thread click
const handleClick = (threadId) => {
setCurrentIndex(threadId);
};
return (
<ul className="chat-list">
{threads.map((thread) => (
<ThreadItem
key={thread.id}
thread={thread}
changeThread={changeThread}
isActive={currentIndex === thread.id}
onClick={() => handleClick(thread.id)}
/>
))}
</ul>
);
};
export default ThreadList;
ThreadItem
import React from 'react';
const ThreadItem = ({ thread, changeThread, isActive, onClick }) => {
return (
<li className="chat-item">
<form onSubmit={changeThread}>
<button
className={isActive ? 'side-menu-links-focus threads' : 'side-menu-links threads'}
onClick={onClick}
>{`#${thread.threadType}`}</button>
</form>
</li>
);
};
export default ThreadItem;

Drop down for a single element in a loop based on an id in react

I'm new to react and have an app that displays some data. I am using a map function to build one component multiple times. When a button is clicked inside of an element more data should be displayed but only in the clicked element. Currently, when I click a button in one element can toggle the display of the additional data for all element as well as store the unique id of the clicked element in a state. I am pretty sure that I need to filter the results and I have seen similar examples but I can't say that I fully understand them. Any tips or more beginner-friendly tutorials are greatly appreciated.
import React, { useState, useEffect } from 'react';
import '../style/skeleton.css'
import '../style/style.css'
export default function Body( student ) {
const [active, setActive] = useState({
activeStudent: null,
});
const [display, setDisplay] = useState(true)
useEffect(() => {
if (display === false) {
setDisplay(true)
} else {
setDisplay(false)
}
}, [active])
const handleClick = (id) => setActive({ activeStudent: id});
return (
<div>
{student.student.map((data) => {
const id = data.id;
return (
<div key={data.id} className="row border">
<div className="two-thirds column">
<h3>{data.firstName} {data.lastName}</h3>
{ display ?
<button onClick={() => handleClick(id)}>-</button>
:
<button onClick={() => handleClick(id)}>+</button> }
{ display ? <div>
<p>{data.addional} additonal data</p>
</div> : null }
</div>
</div>
)
})}
</div>
);
}
Change your code from:
{ display ? <div><p>{data.addional} additonal data</p></div> : null }
To:
{ active.activeStudent === id ? <div><p>{data.addional} additonal data</p></div> : null }

Passing OnChange Function using React for drop down

I have a payment component and custom dropdown component. I'm trying to pass down a function called handlePaymentImageChange from the parent (payment) to child (dropdown) so as to control the image change. However, it does not work well as I expect. What I'm trying to do is displaying the image based on the selection of the dropdown. In my case, if the value = 'Visa' -> render visa image only.
Details: https://codesandbox.io/s/serene-noether-s8pqc?file=/src/components/Payment/Payment.js
In my Payment.js
function Payment() {
const [paymentImage, setPaymentImage] = useState({
id: 0,
value: ""
});
const handlePaymentImageChange = (e) => {
const { name, value } = e.target;
setPaymentImage({
...paymentImage,
[name]: value
});
};
return (
<div className="payment-container">
<Dropdown
title="Select payment"
items={items}
multiSelect={false}
handlePaymentImageChange={handlePaymentImageChange}
/>
{/* render specifed image based on the selected choice */}
//REST RENDER CODE...
// for example, value = Visa -> render visa image only
</div>
);
In my Dropdown.js
import React, { useState } from "react";
import "./Dropdown.css";
function Dropdown({
title,
items = [],
multiSelect = false,
handlePaymentImageChange
}) {
const [open, setOpen] = useState(false);
const [selection, setSelection] = useState([]);
const [selectedValue, setSelectedValue] = useState(title);
//REST DROPDOWN TOGGLE FUNCTION
...
return (
<div className="dropdown-container">
// pass the item.value to change the Payment state, then render the correct image
{open && (
<ul className="dropdown-list">
{items.map((item) => (
<li
className="dropdown-list-item"
key={item.id}
onChange={() => handlePaymentImageChange(item.value)}
>
<button
type="button"
onClick={() => handleOnClick(item)}
value={item.value}
>
<span>{item.value}</span>
<span>{isItemInSelection(item) && "Selected"}</span>
</button>
</li>
))}
</ul>
)
}
</div>
);
}
export default Dropdown;
Any solution?
There are multiple issue,
In Dropdown component you should add eventListener for onClick not onChange.
Inside handlePaymentImageChange method you are using e.target.value for the value. But in your case e itself is the value. So you should write,
setPaymentImage({
...paymentImage,
value: e
});
When you are rendering the image there is no check. So check if value is "Visa" and render visa image and so on.
I have updated the code here please check.

swiper.updateSize()/swiper.update() not working swiperjs React

I am looking to be able to update the size of .swiper-wrapper when there is a DOM event on the current slide. The method swiper.updateSize()/swiper.update() is firing after the DOM event but it is returning undefined. This is odd because when I console.log(swiper) (the instance of swiper) it returns the swiper object instance as expected. I have added the prop autoHeight to so according to the documentation, the height of the .swiper-wrapper should increase/decrease depending on the height of the current slide after the swiper.updateSize()/swiper.update() method is called
Is there something I am doing wrong here?
import React, { useState } from 'react';
function ParentComponent(props) {
let [swiper, setSwiper] = useState(null)
return(
<Swiper
onSwiper={(swiper) => {setSwiper(swiper)}}
autoHeight
>
<ChildComponent
title={"title"}
text={"text"}
swiper={swiper}
/>
</Swiper>
)
}
function ChildComponent(props) {
let [active, setActive] = useState(false)
let swiper = props.swiper
// toggle active class
const handleClick = () => {
if (active) {
setActive(false)
}
else {
setActive(true)
// returns swiper instance
console.log(swiper)
// returns undefined
swiper.updateSize();
}
}
return (
<div className={"infos" + (active ? ' active' : '')}>
<div onClick={handleClick}>
<h4>{props.title}<span></span></h4>
</div>
<div className="text">
<p>{props.text}</p>
</div>
</div>
)
}
export default ChildComponent
ok managed to fix this by setting a timeout and waiting until after the transition had finished (or roughly close to it). The issue seemed to be that the max-height transition hadn't finished by the time the update() method was called on swiper. I also used a different method called .updateAutoHeight() which worked
my code now looks like this:
import React, { useState } from 'react';
function ParentComponent(props) {
let [swiper, setSwiper] = useState(null)
return(
<Swiper
onSwiper={(swiper) => {setSwiper(swiper)}}
autoHeight
>
<ChildComponent
title={"title"}
text={"text"}
swiper={swiper}
/>
</Swiper>
)
}
function ChildComponent(props) {
let [active, setActive] = useState(false)
let swiper = props.swiper
// toggle active class
const handleClick = () => {
if (active) {
setActive(false)
setTimeout(() => {
swiper.updateAutoHeight()
}, 200)
}
else {
setActive(true)
setTimeout(() => {
swiper.updateAutoHeight()
}, 250)
}
}
return (
<div className={"infos" + (active ? ' active' : '')}>
<div onClick={handleClick}>
<h4>{props.title}<span></span></h4>
</div>
<div className="text">
<p>{props.text}</p>
</div>
</div>
)
}
export default ChildComponent
I would suggest changing the timeout ms depending on how long the transition takes to finish. The higher the height of the max-height transition, the longer it will take and therefore the more delay you will need to use on the setTimeout ms for it to be picked up by the JS
All methods of swiper need to be called in setTimeout function

Resources