React <details> - have only one open at a time - reactjs

I have a component with several elements. I'm trying to figure out how to update the code with hooks so that only one element will be open at a time - when a element is open, the other's should be closed. This is the code:
const HowItWorks = ({ content, libraries }) => {
const Html2React = libraries.html2react.Component;
return (
<HowItWorksContainer>
{content.fields.map((tab, i) => {
const [open, setOpen] = useState(false);
const onToggle = () => {
setOpen(!open);
};
return (
<details
key={i}
onToggle={onToggle}
className={`tab ${open ? "open" : "closed"}`}
>
<summary className="tab__heading">
<div className="wrapper">
<p>{tab.heading}</p>
{open ? (
<i className="icon kap-arrow-minus" />
) : (
<i className="icon kap-arrow-plus" />
)}
</div>
</summary>
<div className="tab__content">
<Html2React html={tab.content} />
</div>
</details>
);
})}
</HowItWorksContainer>
);
};

Instead of having the open state be a boolean, make it be the ID of the element that is open. Then you can have a function that returns if the element is open by comparing the state with the ID.
const HowItWorks = ({ content, libraries }) => {
const [open, setOpen] = useState(0); //Use the element ID to check which one is open
const onToggle = (id) => {
setOpen(id);
};
const isOpen = (id) => {
return id === open ? "open" : "closed";
}
const Html2React = libraries.html2react.Component;
return (
<HowItWorksContainer>
{content.fields.map((tab, i) => {
return (
<details
key={i}
onToggle={onToggle}
className={`tab ${isOpen(i)}`}
>
<summary className="tab__heading">
<div className="wrapper">
<p>{tab.heading}</p>
{!!isOpen(i) ? (
<i className="icon kap-arrow-minus" />
) : (
<i className="icon kap-arrow-plus" />
)}
</div>
</summary>
<div className="tab__content">
<Html2React html={tab.content} />
</div>
</details>
);
})}
</HowItWorksContainer>
);
};

Related

DnD-Kit Incorrect behavior while trying to move item to another container in ReactJs

I would appreciate any help with this case, so if you see any minor issue - please, write me. There will be rather a lot of code.
I was trying to implement 'dnd-kit/sortable' into my bug tracker app. I have Kanban board consisting of four repeating column components. I needed to implement dnd-kit to be able to move task cards not only inside of each column, but between columns as well. Current code with sorting task cards in column, but if you try to move a card to any other column - most of the time nothing happens, but sometimes you get the Uncaught TypeError: Cannot read properties of undefined (reading 'id') I red through documentation many times and looked through similar projects in open source, but couldn't find what could be the reason for this bug.
The tasks from TasksContext is object with keys backlog, todo, inProgress, inReview, done and contains array of object. Each object inside of array represents task card.
Dashboard.js
const Dashboard = () => {
const { tasks, setTasks } = useContext(TasksContext)
const [activeId, setActiveId] = useState(null);
const mouseSensor = useSensor(MouseSensor);
const touchSensor = useSensor(TouchSensor);
const sensors = useSensors(mouseSensor, touchSensor)
const fullArray = Array.from(Object.values(tasks).flat())
console.log(fullArray)
const handleDragStart = ({ active }) => setActiveId(active.id);
const handleDragCancel = () => setActiveId(null);
const handleDragEnd = ({active, over}) => {
const { containerId: activeContainer } = active.data.current.sortable
const { containerId: overContainer } = over.data.current.sortable
const oldIndex = tasks[activeContainer].findIndex(obj => obj.id === active.id);
const newIndex = tasks[overContainer].findIndex(obj => obj.id === over.id);
if (active.id !== over.id) {
setTasks((prevTasks) => ({
...prevTasks,
[overContainer]: arrayMove(prevTasks[overContainer], oldIndex, newIndex)
}));
}
setActiveId(null);
}
return (
<div className='relative grid grid-cols-4 gap-6 px-6 grow-0 shrink-0 basis-5/6 overflow-y-scroll'>
<DndContext sensors={sensors} collisionDetection={rectIntersection} onDragStart={handleDragStart} onDragCancel={handleDragCancel} onDragEnd={handleDragEnd}>
<TasksColumn key='to do' title='to do' id='todo' tasks={tasks.todo} />
<TasksColumn key='in progress' title='in progress' id='inProgress' tasks={tasks.inProgress} />
<TasksColumn key='in review' title='in review' id='inReview' tasks={tasks.inReview} />
<TasksColumn key='done' title='done' id='done' tasks={tasks.done} />
<DragOverlay>{activeId ? <TaskCard id={activeId} task={fullArray.filter(task => task?.id === activeId)[0]} /> : null}</DragOverlay>
</DndContext>
</div>
)
}
TasksColumn.js
const TasksColumn = ({ title, id, tasks }) => {
const { setNodeRef } = useDroppable({id});
return (
<div className=''>
<ColumnHeader title={title} id={id} />
<div className="h-3 w-full border-b-2 border-grayDark" />
<SortableContext items={tasks} id={id} strategy={verticalListSortingStrategy}>
<div ref={setNodeRef} className=''>
{tasks.map(task => (
<Draggable key={task.name} id={task.id} task={task} />
))}
</div>
</SortableContext>
</div>
)
}
Draggable.js
const Draggable = ({ id, task }) => {
const { setNodeRef, transform, transition, isDragging, } = useSortable({id});
const style = {
transform: CSS.Translate.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
return (
<div ref={setNodeRef} style={style}>
<TaskCard id={id} task={task} />
</div>
)
}
TaskCard.js
const TaskCard = ({ id, task }) => {
const { attributes, listeners, setActivatorNodeRef } = useSortable({id});
return (
<div className="py-4 border-b-2 border-grayLight">
<div className="">
<p className="">{task.deadline}</p>
<p className="">{task.priority}</p>
</div>
<ArrowsPointingOutIcon className='rotate-45 w-5 h-5 outline-none' ref={setActivatorNodeRef} {...listeners} {...attributes} />
<p className="">{task.name}</p>
<div className="">
<p className="">{task.author}</p>
<p className="">{task.time}</p>
</div>
</div>
)
}

React button onClick requires two click to work second time

When the img is pressed the side menu opens fine but when i close the side menu and try to open it again by pressing the img it requires two clicks to open. how do i make it so it works with one click even after i open and close the side menu?
const Header = () => {
let [isOpen, setIsOpen] = useState(false);
return (
<header className='homepage-header'>
<img src={menusvg} alt='' onClick={() => setIsOpen((isOpen) => !isOpen)} />
{
isOpen ? <SideMenu /> : null
}
<h1>Main Header</h1>
</header>
)
}
note: i have another button inside the SideMenu componenet that closes the menu.
SideMenu:
function SideMenu() {
let [isOpen, setIsOpen] = useState(true);
return (
<>
{
isOpen ? <div className='side-menu'>
<p>side menue</p>
<i class="fas fa-times" onClick={() => setIsOpen((isOpen) => !isOpen)}></i>
</div>
: null
}
</>
)
}
You are using two different states, pass isOpen and setIsOpen as props to SideMenu
Try this code
function SideMenu({ isOpen, setIsOpen }) {
return (
<>
{isOpen ? (
<div className="side-menu">
<p>side menue</p>
<button onClick={() => setIsOpen(isOpen => !isOpen)}>Close</button>
</div>
) : null}
</>
);
}
--
export default function App() {
let [isOpen, setIsOpen] = React.useState(false);
return (
<header className="homepage-header">
<img
src={'https://picsum.photos/200'}
alt=""
onClick={() => setIsOpen(isOpen => !isOpen)}
/>
{isOpen ? <SideMenu isOpen={isOpen} setIsOpen={setIsOpen} /> : null}
<h1>Main Header</h1>
</header>
);
}
Sample Code: https://stackblitz.com/edit/react-j4rxot?file=src%2FApp.js
The logic you have used to make isOpen true and false is bit wierd,(at least for me), although feels correct.
A simple way would be,
const Header = () => {
let [isOpen, setIsOpen] = useState(false);
return (
<header className='homepage-header'>
<img src={menusvg} alt='' onClick={() => isOpen ? setIsOpen(false) : setIsOpen(true)} />
{
isOpen ? <SideMenu /> : null
}
<h1>Main Header</h1>
</header>
)
}
Hope it works!

React array item selection

I am trying to click on one card of a dynamically created list using map(). I want to click on one card from the array and add a class to it, while at the same time deselecting the other card that was previously clicked. How can I accomplish this? This is what I have so far:
const CardList = () => {
return (
<div className='card-list'>
{CardData.map(({ id, ...otherData }) => (
<Card key={id} {...otherData} />
))}
</div>
);
};
export default CardList;
const Card = ({
headline,
time,
views,
thumbImg,
trainerImg,
workouts,
id
}) => {
const [isSelected, setIsSelected] = useState(false);
const [clickId, setClickId] = useState('');
function handleClick(id) {
setIsSelected(!isSelected);
setClickId(id);
}
return (
<div
className={`card ${isSelected && clickId === id ? 'clicked' : ''}`}
onClick={() => handleClick(id)}
>
<div className='thumbnail-div'>
<img className='thumbnail-img' src={thumbImg} alt='video' />
{workouts ? (
<div className='workout-overlay'>
<p>{workouts}</p>
<p className='workouts'>workouts</p>
</div>
) : null}
</div>
<div className='card-info'>
<div className='card-headline'>
<p>{headline}</p>
<img src={trainerImg} alt='trainer' />
</div>
{time && views ? (
<div className='trainer-data'>
<span>
<i className='glyphicon glyphicon-time'></i>
{time}
</span>
<span>
<i className='glyphicon glyphicon-eye-open'></i>
{views}
</span>
</div>
) : null}
</div>
</div>
);
};
export default Card;
The parent component should control what card is clicked. Add className property to card component:
const Card = ({
//...
className,
onClick
}) => {
//...
return (
<div
className={`card ${className}`}
onClick={() => onClick(id)}
>...</div>
)
}
In parent component pass the className 'clicked' and add the onClick callback to set the selected card:
const CardList = () => {
const [isSelected, setIsSelected] = useState(null);
const handleClick = (id) => {
setIsSelected(id);
}
return (
<div className='card-list'>
{CardData.map(({ id, ...otherData }) => (
<Card key={id} className={isSelected===id && 'clicked'} onClick ={handleClick} {...otherData} />
))}
</div>
);
};
You can do something like this.
First you don't have to set state to each card. Instead Lift state Up.
You define which card is selected in parent so you can pass that to children and add classes if current selected is matching that children.
const CardList = () => {
const [isSelected, setIsSelected] = useState();
const handleCardClick = (id) => {
setIsSelected(id);
}
return (
<div className='card-list'>
{CardData.map(({ id, ...otherData }) => (
<Card key={id} {...otherData} handleClick={handleCardClick} isSelected={isSelected}/>
))}
</div>
);
};
export default CardList;
const Card = ({
headline,
time,
views,
thumbImg,
trainerImg,
workouts,
id,
isSelected,
handleClick
}) => {
return (
<div
className={`card ${isSelected === id ? 'clicked' : ''}`}
onClick={() => handleClick(id)}
>
<div className='thumbnail-div'>
<img className='thumbnail-img' src={thumbImg} alt='video' />
{workouts ? (
<div className='workout-overlay'>
<p>{workouts}</p>
<p className='workouts'>workouts</p>
</div>
) : null}
</div>
<div className='card-info'>
<div className='card-headline'>
<p>{headline}</p>
<img src={trainerImg} alt='trainer' />
</div>
{time && views ? (
<div className='trainer-data'>
<span>
<i className='glyphicon glyphicon-time'></i>
{time}
</span>
<span>
<i className='glyphicon glyphicon-eye-open'></i>
{views}
</span>
</div>
) : null}
</div>
</div>
);
};
export default Card;

React child not re-rendered when parents props change

I'm having some issues with child re-rendering, I pass methods to children to see if a button should be displayed or not but when the state of the parent changes, the children are not re-rendered.
I tried with the disabled attribute for the button but didn't work either.
Here's my code (I removed unnecessary part):
function Cards(props) {
const isCardInDeck = (translationKey) => {
return props.deck.some(
(card) => !!card && card.translationKey === translationKey
);
};
const addToDeck = (card) => {
if (!isCardInDeck(card.translationKey) && !!card) {
props.deck.push(card);
}
};
const removeFromDeck = (card) => {
if (isCardInDeck(card.translationKey) && !!card) {
var index = props.deck.findIndex(
(c) => c.translationKey === card.translationKey
);
props.deck.splice(index, 1);
}
};
return (
<div className="cardsContent">
<div className="cards">
{cardList.length > 0 ? (
cardList.map((item, index) => {
return (
<Card key={index} card={item} addToDeckDisabled={isCardInDeck(item.translationKey)} addToDeckClick={addToDeck} removeFromDeckClick={removeFromDeck} />
);
})
) : (
<span>
<FormattedMessage id="app.cards.label.no.card.found" defaultMessage="No card found with filter."/>
</span>
)}
</div>
</div>
);
}
function Card(props) {
const toggleShowDescription = () => {
if (!showDescription) {
setShowDescription(!showDescription);
}
};
return (
<div onClick={toggleShowDescription} onBlur={toggleShowDescription} >
<img src={"../images/cards/" + props.card.image} alt={props.card.image + " not found"} />
{showDescription ? (
<div className="customCardDetail">
<div className="cardName"></div>
<div className="cardType">
{props.addToDeckDisabled ? (
<Button onClick={() => { props.removeFromDeckClick(props.card);}} startIcon={<RemoveIcon />}>
Remove from deck
</Button>
) : (
<Button onClick={() => { props.addToDeckClick(props.card); }} startIcon={<AddIcon />}>
Add to deck
</Button>
)}
</div>
<div className="cardDescription">
<span>
<FormattedMessage id={props.card.description} defaultMessage={props.card.description} />
</span>
</div>
</div>
) : (
""
)}
</div>
);
}
You code does not update state. Cards mutates the props that it is receiving.
To use state in a functional component in React you should use the useState hook.
Cards would then look something like this:
function Cards(props) {
const [deck, setDeck] = useState(props.initialDeck)
const isCardInDeck = (translationKey) => {
return deck.some(
(card) => !!card && card.translationKey === translationKey
);
};
const addToDeck = (card) => {
if (!isCardInDeck(card.translationKey) && !!card) {
setDeck([...deck, card])
}
};
const removeFromDeck = (card) => {
if (isCardInDeck(card.translationKey) && !!card) {
setDeck(deck.filter(deckItem => deckItem.translationKey !== card.translationKey))
}
};
return (
<div className="cardsContent">
<div className="cards">
{cardList.length > 0 ? (
cardList.map((item, index) => {
return (
<Card key={index} card={item} addToDeckDisabled={isCardInDeck(item.translationKey)} addToDeckClick={addToDeck} removeFromDeckClick={removeFromDeck} />
);
})
) : (
<span>
<FormattedMessage id="app.cards.label.no.card.found" defaultMessage="No card found with filter."/>
</span>
)}
</div>
</div>
);
}

React - Warning: Each child in a list should have a unique "key" prop

In this simple React App, I don't understand why I get the following warning message:
Warning: Each child in a list should have a unique "key" prop.
To me it seems that I put the key at the right place, in form of key={item.login.uuid}
How can I get rid of the warning message?
Where would be the right place to put the key?
App.js
import UserList from './List'
const App = props => {
const [id, newID] = useState(null)
return (
<>
<UserList id={id} setID={newID} />
</>
)
}
export default App
List.js
const UserList = ({ id, setID }) => {
const [resources, setResources] = useState([])
const fetchResource = async () => {
const response = await axios.get(
'https://api.randomuser.me'
)
setResources(response.data.results)
}
useEffect(() => {
fetchResource()
}, [])
const renderItem = (item, newID) => {
return (
<>
{newID ? (
// User view
<div key={item.login.uuid}>
<div>
<h2>
{item.name.first} {item.name.last}
</h2>
<p>
{item.phone}
<br />
{item.email}
</p>
<button onClick={() => setID(null)}>
Back to the list
</button>
</div>
</div>
) : (
// List view
<li key={item.login.uuid}>
<div>
<h2>
{item.name.first} {item.name.last}
</h2>
<button onClick={() => setID(item.login.uuid)}>
Details
</button>
</div>
</li>
)}
</>
)
}
const user = resources.find(user => user.login.uuid === id)
if (user) {
// User view
return <div>{renderItem(user, true)}</div>
} else {
// List view
return (
<ul>
{resources.map(user => renderItem(user, false))}
</ul>
)
}
}
export default UserList
The key needs to be on the root-level element within the loop. In your case, that's the fragment (<>).
To be able to do that, you'll need to write it out fully:
const renderItem = (item, newID) => {
return (
<Fragment key={item.login.uuid}>
{newID ? (
...
)}
</Fragment>
);
}
(You can add Fragment to your other imports from react).
Note that the fragment isn't actually needed in your example, you could drop it and keep the keys where they are since then the <div> and <li> would be the root element:
const renderItem = (item, newId) => {
return newID ? (
<div key={item.login.uuid}>
...
</div>
) : (
<li key={item.login.uuid}>
...
</li>
)
}
What if you create 2 separate components, one for the user view and one for the list item. That way you only need to pass the user prop. Also, use JSX and pass wht key from there.
const UserList = ({ id, setID }) => {
const [resources, setResources] = useState([])
const fetchResource = async () => {
const response = await axios.get(
'https://api.randomuser.me'
)
setResources(response.data.results)
}
useEffect(() => {
fetchResource()
}, [])
const User = ({user}) => (
<div key={user.login.uuid}>
<div>
<h2>
{user.name.first} {user.name.last}
</h2>
<p>
{user.phone}
<br />
{user.email}
</p>
<button onClick={() => setID(null)}>
Back to the list
</button>
</div>
</div>
)
const ListItem = ({user}) => (
<li key={user.login.uuid}>
<div>
<h2>
{user.name.first} {user.name.last}
</h2>
<button onClick={() => setID(user.login.uuid)}>
Details
</button>
</div>
</li>
)
const user = resources.find(user => user.login.uuid === id)
if (user) {
// User view
return <User user={user}</div>
} else {
// List view
return (
<ul>
{resources.map((user, index) => <ListItem key={index} user={user} />)}
</ul>
)
}
}
export default UserList

Resources