I want to make a component in the front page that displays list of vehicles with navigation buttons that shows list of the selected button.
Is there a better way I could write this code below because I want the list of categories to be much longer? Or are there other methods I can use other than useState & useEffect? I'm using Next.js
const Vehicles = () => {
const [selectedCategory, setSelectedCategory] = useState("cars")
const [cars, setCars] = useState(true)
const [bikes, setBikes] = useState(false)
const [motorcycles, setMotorcyles] = useState(false)
const [scooters, setScooters] = useState(false)
useEffect(() => {
if (selectedCategory === "cars") {
setCars(true)
setBikes(false)
setMotorcyles(false)
setScooter(false)
}
if (selectedCategory === "bikes") {
setBikes(true)
setCars(false)
setMotorcyles(false)
setScooters(false)
}
if (selectedCategory === "motorcycles") {
setMotorcyles(true)
setCars(false)
setBikes(false)
setScooters(false)
}
if (selectedCategory === "scooters") {
setScooters(true)
setCars(false)
setBikes(false)
setMotorcyles(false)
}
}, [selectedCategory])
return (
<div>
<div>
<button onClick={() => setSelectedCategory("cars")}> Cars </button>
<button onClick={() => setSelectedCategory("bikes")}> Bikes </button>
<button onClick={() => setSelectedCategory("motorcycles")}> Motorcycles </button>
<button onClick={() => setSelectedCategory("scooters")}> Scooters </button>
</div>
{cars && (
<div>
<Cars />
</div>
)}
{bikes && (
<div>
<Bikes />
</div>
)}
{motorcycles && (
<div>
<Motorcycles />
</div>
)}
{scooters && (
<div>
<Scooters />
</div>
)}
</div>
);
};
export default Vehicles;
These things like abstraction and code refactor tend to differ from person to person. But what I would generally prefer is something between abstraction and readability. Since readability is much more important I would go along this way
const allCategory = {
cars: 0,
bikes: 1,
motorcycles: 2,
scooters: 3
}
const Vehicles = () => {
// set cars as default category. If you don't want it change it to be any integer before 0
const [category, setCategory] = useState(allCategory.cars)
return (
<div>
<div>
<button onClick={() => setCategory(allCategory.cars)}> Cars </button>
<button onClick={() => setCategory(allCategory.bikes)}> Bikes </button>
<button onClick={() => setCategory(allCategory.motorcycles)}> Motorcycles </button>
<button onClick={() => setCategory(allCategory.scooters)}> Scooters </button>
</div>
<div>
{category === allCategory.cars && <Cars />}
{category === allCategory.bikes && <Bikes />}
{category === allCategory.motorcycles && <Motorcycles />}
{category === allCategory.scooters && <Scooters />}
</div>
</div>
);
};
export default Vehicles;
If you want to go absolutely crazy. You can decrease this to be in about 4-5 lines in return statement. But I prefer this to be much cleaner
I think you could do something more like
const Vehicles = () => {
const [selectedCategory, setSelectedCategory] = useState("cars")
const renderCat = (category) =>{
switch(category){
case "cars":
return <Cars />
//etc...
}
}
return (
<div>
<div>
<button onClick={() => setSelectedCategory("cars")}> Cars </button>
<button onClick={() => setSelectedCategory("bikes")}> Bikes </button>
<button onClick={() => setSelectedCategory("motorcycles")}> Motorcycles </button>
<button onClick={() => setSelectedCategory("scooters")}> Scooters </button>
</div>
<div>
{renderCat(selectedCategory)}
</div>
</div>
);
};
export default Vehicles;
You could do like this:
const categories = [
{
name: "cars",
title: "Cars",
component: Cars
},
{
name: "bikes",
title: "Bikes",
component: Bikes
},
{
name: "motorcyles",
title: "Motorcyles",
component: Motorcyles
},
{
name: "scooters",
title: "Scooters",
component: Scooters
}
];
const Vehicles = () => {
const [selectedCategory, setSelectedCategory] = useState("cars")
return (
<div>
<div>
{categories.map(({ name, title }) => (
<button onClick={() => setSelectedCategory(name)}>
{title}
</button>
))}
</div>
{categories.map(({ component: Component, name }) => {
if(selectedCategory !== name) return;
return (
<div>
<Component />
</div>
);
})}
</div>
);
};
export default Vehicles;
The category objects can be different depending on what you want to display. Having both name and title is not essential, it's just an example.
Related
I'm mapping an array of items. Some items must display videos and others, images. I made 2 functions for each display and I'm using useState to toggle them.
export default function App() {
//SIMPLE USESTATE TO TOGGLE WINDOW
const [open, setOpen] = useState(false);
//THE ARRAY (1 contains an image and the other a video)
const itemsArray = [
{
name: "Item1",
imageUrl: "some image url"
},
{
name: "Item2",
videoUrl: "some video url"
}
];
//RENDER THIS IF ITEM IS IMAGE
const ifImage = (i) => {
return (
<div onClick={() => setOpen(!open)}>
<img src={i} alt="cat" />
</div>
);
};
//RENDER THIS IF ITEM IS VIDEO
const ifVideo = (v) => {
return (
<div className="window-on-top" onClick={() => setOpen(!open)}>
<iframe>Some Video</iframe>
</div>
);
};
return (
<div className="App">
<h3>One button shows a cat photo and the other a cat video</h3>
{itemsArray.map((item) => {
return (
<div key={item.name}>
<button className="niceBtn" onClick={() => setOpen(!open)}>
{item.name}
</button>
{/* NESTING CONDITIONALS OR SOMETHING TO MAKE THIS WORK */}
{open ? {
{item.imageUrl ? ifImage(item.imageUrl): null}
||
{item.videoUrl ? ifVideo(item.videoUrl): null}
} : null}
</div>
);
})}
</div>
);
}
I'm obviously wrong... Need some help understanding how to solve this...
Here is a Sandbox with the code to watch it properly.
SANDBOX
I'd put the conditions inside the subfunctions instead, which should make it easy to understand what's going on.
const tryImage = item => (
!item.imageUrl ? null : (
<div onClick={() => setOpen(!open)}>
<img src={item.imageUrl} alt="cat" />
</div>
));
const tryVideo = item => (
!item.videoUrl ? null : (
<div onClick={() => setOpen(!open)}>
<img src={item.videoUrl} alt="cat" />
</div>
));
return (
<div className="App">
<h3>One button shows a cat photo and the other a cat video</h3>
{itemsArray.map((item) => {
return (
<div key={item.name}>
<button className="niceBtn" onClick={() => setOpen(!open)}>
{item.name}
</button>
{open && ([
tryImage(item),
tryVideo(item),
])}
</div>
);
})}
</div>
);
Not sure, but you also probably want a separate open state for each item in the array, rather than a single state for the whole app.
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;
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>
);
};
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>
);
}
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