React child not re-rendered when parents props change - reactjs

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>
);
}

Related

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 <details> - have only one open at a time

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>
);
};

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

How to save key in localStorage to be retrieve later when you go back to your page in ReactJS

I have a page, and I want to save the key to be retrieved later. This key is use to determine the last selected active item in the carousel. What do I need to do using localStorage in ReactJS?
import React, { Fragment, Component } from 'react'
import { truncateString } from '#helpers'
import './styles.css'
class RoutineSidebar extends Component {
handleOnClick = key => {
const { currentSlideKey } = this.props;
const isExerciseDone = this.props.skipExerciseValidation(currentSlideKey);
if(isExerciseDone || key < this.props.currentSlideKey) {
if(this.props.skipExerciseValidation(key === 0 ? 0 : key - 1)) {
this.props.setKey(key);
}
} else {
if(key === this.props.currentSlideKey + 1) {
this.props.openSkipExerModal();
}
}
React.useEffect(() => {
localStorage.setItem('selectedRoutine', key);
}, [key]);
}
checkExerciseStatusSkipped = key => {
const { routineList } = this.props;
return routineList[key].skipped;
};
checkExerciseStatusDone = key => {
const { routineList } = this.props;
return routineList[key].done;
}
checkExercisesSelected = key => {
const { routineList } = this.props;
return routineList[key];
}
render() {
const { exercises, currentSlideKey } = this.props;
const todaysRoutineThumbnails = exercises.map((exercise, key) => {
return (
<div key={key} onClick={() => this.handleOnClick(key)} className={key === currentSlideKey ? 'thumbnail-container selected-exercise' : 'thumbnail-container'}>
<div className="row todays-routine">
<div className="col-sm-6">
{
this.checkExerciseStatusSkipped(key) ? <Fragment><i className="fas fa-times-circle status-indicator-warning" />
<div className="scThumb">
<img className='active-thumbnail img-opaque' alt="todays-routine-thumbnail" src={exercise.thumbnail} />
</div>
</Fragment>
: this.checkExerciseStatusDone(key) ? <Fragment><i className="fas fa-check-circle status-indicator-done" />
<div className="scThumb">
<img className='active-thumbnail img-opaque' alt="todays-routine-thumbnail" src={exercise.thumbnail} />
</div>
</Fragment>
: !this.checkExerciseStatusDone(key) && !this.checkExerciseStatusSkipped(key) && <Fragment><div className="routine-exercise-counter">{key + 1}</div><div className="scThumb">
<img className='active-thumbnail' alt="todays-routine-thumbnail" src={exercise.thumbnail} />
</div>
</Fragment>
}
</div>
<div className="col-sm-6">
<div className="thumbnail-info clearfix">
<p className="thumbnail-title">{truncateString(exercise.exerciseName, 30)}</p>
<p className="thumbnail-description">This is the best exercise for back pain ever made</p>
</div>
</div>
</div>
</div>
)
})
return (
<div className="todays-routine-container">
<h1>{this.props.header}</h1>
{todaysRoutineThumbnails}
</div>
)
}
}
export default RoutineSidebar;
You can't use useEffect hook inside a class-based component.
to use localStorage with React you don't need anything, just use it directly.
I guess if you replaced your useEffect code with this code, it will work fine.
setTimeout(() => {
window.localStorage.setItem("selectedRoutine", key);
}, 0);
Just you need to create one util.js file for this kind of comman usages and paste below code and use these functions by just import.
export const getLocalStorage = (key) => {
const localStorageData = localStorage.getItem(key);
if (localStorageData) {
return JSON.parse(localStorageData);
} else {
return null;
}
}
export const setLocalStorage = (key, value) => {
if (key && value) {
localStorage.setItem(key, JSON.stringify(value));
}
}
Please try this
import React, { Fragment, Component } from 'react'
import { truncateString } from '#helpers'
import './styles.css'
class RoutineSidebar extends Component {
constructor(props){
super(props);
this.setState={
activeCarouselItem: ""
}
}
componentDidMount(){
this.setState({
activeCarouselItem: localStorage.getItem('selectedRoutine')?localStorage.getItem('selectedRoutine'): ""
})
}
handleOnClick = key => {
const { currentSlideKey } = this.props;
const isExerciseDone = this.props.skipExerciseValidation(currentSlideKey);
if(isExerciseDone || key < this.props.currentSlideKey) {
if(this.props.skipExerciseValidation(key === 0 ? 0 : key - 1)) {
this.props.setKey(key);
}
} else {
if(key === this.props.currentSlideKey + 1) {
this.props.openSkipExerModal();
}
}
// React.useEffect(() => {
// localStorage.setItem('selectedRoutine', key);
// }, [key]);
this.setState({activeCarouselItem: key })
}
checkExerciseStatusSkipped = key => {
const { routineList } = this.props;
return routineList[key].skipped;
};
checkExerciseStatusDone = key => {
const { routineList } = this.props;
return routineList[key].done;
}
checkExercisesSelected = key => {
const { routineList } = this.props;
return routineList[key];
}
render() {
const { exercises, currentSlideKey } = this.props;
const todaysRoutineThumbnails = exercises.map((exercise, key) => {
return (
<div key={key} onClick={() => this.handleOnClick(key)} className={key === currentSlideKey ? 'thumbnail-container selected-exercise' : 'thumbnail-container'}>
<div className="row todays-routine">
<div className="col-sm-6">
{
this.checkExerciseStatusSkipped(key) ? <Fragment><i className="fas fa-times-circle status-indicator-warning" />
<div className="scThumb">
<img className='active-thumbnail img-opaque' alt="todays-routine-thumbnail" src={exercise.thumbnail} />
</div>
</Fragment>
: this.checkExerciseStatusDone(key) ? <Fragment><i className="fas fa-check-circle status-indicator-done" />
<div className="scThumb">
<img className='active-thumbnail img-opaque' alt="todays-routine-thumbnail" src={exercise.thumbnail} />
</div>
</Fragment>
: !this.checkExerciseStatusDone(key) && !this.checkExerciseStatusSkipped(key) && <Fragment><div className="routine-exercise-counter">{key + 1}</div><div className="scThumb">
<img className='active-thumbnail' alt="todays-routine-thumbnail" src={exercise.thumbnail} />
</div>
</Fragment>
}
</div>
<div className="col-sm-6">
<div className="thumbnail-info clearfix">
<p className="thumbnail-title">{truncateString(exercise.exerciseName, 30)}</p>
<p className="thumbnail-description">This is the best exercise for back pain ever made</p>
</div>
</div>
</div>
</div>
)
})
return (
<div className="todays-routine-container">
<h1>{this.props.header}</h1>
{todaysRoutineThumbnails}
</div>
)
}
}
export default RoutineSidebar

How to add class to the current element in map statement

I am using a class component in react and would like to know how I can add a CSS class to the current i.e clicked element which is inside a map statement. I would like to do it using state.
<div key={q.id} id={q.id}>
<h2 className={this.state.title}>{q.title}</h2>
<h3>{q.questionText}</h3>
<div key={q.id}>
{q.options.map((opt, index) => (
<div
key={opt.id}
val={opt.val}
ref={this.options}
className={index === this.state.clickedItem ? 'myclass' : null}
onClick={() => this.setState({ clickedItem: index })}>
<p onClick={this.submitQuestion} ref={this.correctRef}>
{opt.text}
</p>
</div>
))}
</div>
Here your state
state = {clickedItem: 0}
in render
yourArray.map((el, index) => {
<div
onClick={() => this.setState({clickedItem: index})}
key={index}
className={index === this.state.clickedItem ? 'Your ClassName' : null}>
{el.name}
</div>
})
In functional with useState hook, without class.
Hope this can help.
https://codesandbox.io/s/blissful-boyd-6px43?file=/src/App.js
import "./styles.css";
/*
.is-checked {
background-color: #901c1c;
color: white;
}
*/
import React, { useState } from "react";
const App = () => {
const tags = ["portrait", "événements", "voyage", "animaux"];
const [clickedItem, setClickedItem] = useState(null);
const handleCSS = (e) => {
e.preventDefault();
let selectedTag = e ? parseInt(e.target.id, 10) : null;
setClickedItem(selectedTag);
console.log(">> clickedItem", clickedItem);
};
return (
<>
<div className="App">
<h1>Hello !</h1>
</div>
<div>
{tags.map((tag, index) => {
return (
<button
type="button"
key={index}
onClick={handleCSS}
id={index}
className={index === clickedItem ? "is-checked" : null}
>
{`#${tag}`}
</button>
);
})}
</div>
</>
);
};
export default App;

Resources