Route within list-item - reactjs

I want to render a Route inside a list item. When I click on the Link the li shall open and reveal the child component. I have tried this simple solution:
const Topics = ({ match }) => (
<div>
<h2>Topics</h2>
<ul>
{
topics.map(topic => (
<li key={topic.id}>
<Link to={`${match.url}/${link.path}`}>
{topic.title}
</Link>
<Route path={`${match.path}/${topic.path}`}
component={Topic} />
</li>
))
}
</ul>
</div>
)
It works, but I'm not sure this is proper coding. Another variant I wrote is this:
class Topics extends Component {
state = { open: false }
handleClick = value => {
this.setState({ open: value })
}
render () {
const { match } = this.props
const { open } = this.state
return (
<div>
<h1>Topics</h1>
<ul>
{ topics.map(({ name, id }) => (
<li key={id}
className={(open===id) ? 'open' : null}>
<Link to={`${match.url}/${id}`}
onClick={() => this.handleClick(id)}>
{name}
</Link>
<div>
{
open===id ?
<Route path={`${match.path}/:topicId`}
render={({ match }) => (
<Topic
topic={topics.find(({ id }) =>
id === match.params.topicId)}
match={match}
/>)}
/> : null
}
</div>
</li>
))}
</ul>
</div>
)
}
}
Is anyone of these solutions good and proper? Any other suggestions on how to solve this?

I would recommend you define a new component for the Route and use the children property.

Do you mean something like this?
const ListItemLink = ({ to, ...rest }) => (
<Route path={to} children={({ match }) => (
<li className={match ? 'active' : ''}>
{ log(match) }
<Link to={to} {...rest}>
{ to }
</Link>
{ match ?
<h1>{match.url}</h1> : null }
</li>
)} />
)
const Topics = ({ match }) => (
<div>
<h2>Topics</h2>
<ul>
{
LinksArray.map(link => (
<ListItemLink key={link.id}
to={`${match.url}/${link.path}`} />
))
}
</ul>
</div>
)

Related

How can I insert row div for every 3rd entry in props?

I've got a set of dashboards that display in bootstrap cards on a front page and I would like to wrap them in a div with the class row for every 3rd entry. I was thinking about marking my dashboard component with the DB id from props and use a modulus function, but that will cause problems if an ID is deleted
Dashboard component:
export type DashboardProps = {
id: number
title: string
description: string
}
const Dashboard: React.FC<{ dashboard: DashboardProps }> = ({ dashboard }) => {
return (
<>
<div className="col-sm-12 col-lg-4">
<div className="card bg-light h-100">
<div className="card-header">
{dashboard.title}
</div>
<div className="card-body d-flex flex-column">
<p className="card-text">
{dashboard.description}
</p>
<a className="btn btn-info text-center mt-auto"
onClick={() =>
Router.push("/dashboard/[id]", `/dashboard/${dashboard.id}`)
}
>Go to dashboard</a>
</div>
</div>
</div>
</>
)
}
export default Dashboard
Index page:
type Props = {
dashboards: DashboardProps[]
}
export const getServerSideProps: GetServerSideProps = async () => {
const dashboards = await prisma.dashboard.findMany({
orderBy: { id: "asc", },
})
return {
props: JSON.parse(JSON.stringify({ dashboards })),
}
}
const Index: React.FC<Props> = (props) => {
const { data: session, status } = useSession()
if (status === "loading") {
return (
<Spinner />
)
}
if (!session) {
return (
<Layout>
<AccessDenied />
</Layout>
)
}
return (
<Layout>
<h1>Dashboards</h1>
{props.dashboards.map((dashboard) => (
<Dashboard dashboard={dashboard} />
))}
</Layout>
)
}
export default Index
I could also potentially wrap them in a single div with class row, but I would need to enforce a top/bottom margin so the cards don't stack right on top of each other. Any tips to get me rolling on this would be greatly appreciated!
.map provides index, you this to find every 3rd element.
//...
{
props.dashboards.map((dashboard, index) =>
(index + 1) % 3 === 0 ? (
<div>
<Dashboard key={dashboard.id} dashboard={dashboard} />
</div>
) : (
<Dashboard key={dashboard.id} dashboard={dashboard} />
)
)
}

React Navigation Filter only works when going back to the home screen

within my React ecommerce store, I have navigation links that take you to the products filtered by whichever link you selected. There are also filter buttons within the product display. Everything works fine, however, the navigation links only work when I go back to the home page.
Example. I can click on mens & it filters to mens items. Then I can go home and from there click on womens and it filters to womens items. However, if I click on mens and then click on womens items, I see the path change in the url to womens but the filtered products on mens.
Here is a small sample of the code from the 3 components I believe are leading to this.:
App.js
class App extends React.Component {
state= {
cart: []
}
updateCart = (cartItem) => {
this.setState({cart: [...this.state.cart, cartItem]})
}
removeFromCart = (id) => {
this.setState({cart: this.state.cart.filter(i => i.id !== id)})
}
render() {
return (
<div className="App">
<Router>
<Navigation/>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/men" element={ <ButtonMenu updateCart={this.updateCart} path={'men'}/>} />
<Rou</Routes>
<FooterNav/>
</Router>te path="/women" element={ <ButtonMenu updateCart={this.updateCart} path={'women'}/>} /
</div>
)
}
}
export default App;
Navigation.js
class Navigation extends Component{
render(){
return(<>
<div>
<nav className="navbar navbar-expand-lg ">
<div className="container-fluid">
<a className="navbar-brand" href="#"><img src = {logo} alt="logo" width = "150rem"/></a>
<button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="navbarSupportedContent">
<ul className="nav justify-content-center myNav " >
<li className="nav-item">
<Link className="nav-link active" aria-current="page" to="/">Home</Link>
</li>
<li className="nav-item">
<Link className="nav-link " aria-current="page" to="/men">Men</Link>
</li>
<li className="nav-item">
<Link className="nav-link " aria-current="page" to="/women">Women</Link>
</li>
</ul>
</div>
</div>
</nav>
</div>
</>
)
}
}
export default Navigation;
ButtonMenu.js
export const ProductList =[ 70+ objects with category of men, women...etc ]
const CATEGORIES = ['All', 'Men', 'Women', 'Jersey', "Hats",'Shirts', 'Accessories', 'Collectable', 'Top-Products', 'New-Arrivals'];
class ButtonMenu extends Component {
constructor(props) {
super(props);
this.state = {
products: ProductList
};
}
getCategory = (category) => {
if (category === "all") {
this.setState({ products: ProductList });
} else {
const filter = ProductList.filter((d) => d.category === category);
if (filter) {
this.setState({ products: filter });
}
}
};
componentDidMount(){
const path = this.props.path
if (path === "all") {
this.setState({ products: ProductList });
} else {
const filter = ProductList.filter((d) => d.category === path);
if (filter) {
this.setState({ products: filter });
}
}
}
render() {
return (
<>
<div className="mainbtn-menu-container">
{CATEGORIES.map((item, index) => (
<button
key={index}
className="main-btn"
onClick={() => { this.getCategory(item.toLowerCase()) } }
value={item.toLowerCase()}
>
{item}
</button>
))}
</div>
<Products products={this.state.products} updateCart={this.props.updateCart} />
</>
);
}
}
export default ButtonMenu;
The ButtonMenu component doesn't handle when props update to update any local state. This would be handled by the componentDidUpdate lifecycle hook. That said, it's considered anti-pattern to duplicate React state or store derived state. You can simply filter the ProductList array inline when rendering based on the path prop.
Example:
class ButtonMenu extends Component {
state = {
category: 'all',
}
render() {
return (
<>
<div className="mainbtn-menu-container">
{CATEGORIES.map((item, index) => (
<button
key={index}
className="main-btn"
onClick={() => this.setState({
category: item.toLowerCase(),
})}
value={item.toLowerCase()}
>
{item}
</button>
))}
</div>
<Products
products={ProductList
.filter(el => {
if (this.state.category === 'all') {
return true;
}
return el.category === this.state.category;
})
.filter(el => {
if (this.props.path === 'all') {
return true;
}
return el.gender === path;
})
}
updateCart={this.props.updateCart}
/>
</>
);
}
}
I suggest converting ButtonMenu to a function component and rendering a single route with a route param that can be read by the component.
Example:
<Router>
<Navigation/>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/:gender"
element={<ButtonMenu updateCart={this.updateCart} />}
/>
</Routes>
<FooterNav/>
</Router>
...
import { useParams } from 'react-router-dom';
const ButtonMenu = ({ updateCart }) => {
const { gender } = useParams();
const [category, setCategory] = React.useState('all');
return (
<>
<div className="mainbtn-menu-container">
{CATEGORIES.map((item, index) => (
<button
key={index}
className="main-btn"
onClick={() => this.setCategory(item.toLowerCase())}
value={item.toLowerCase()}
>
{item}
</button>
))}
</div>
<Products
products={ProductList
.filter(el => {
if (category === 'all') {
return true;
}
return el.category === category;
})
.filter(el => {
if (gender === 'all') {
return true;
}
return el.gender === gender;
})
}
updateCart={updateCart}
/>
</>
);
}

How to pass the list "key" value as a props in nested component?

(I apologize for the ugly code in advance -- currently refactoring)
I'm making a Table of content where the nested content appear when I click on its parent component.
For my logic, I need to pass the value of the list key to its children but I keep receiving an undefined error or nothing at all. I tried to pass the value like this: key={node2.objectId} and keyId={node2.objectId}
I read the specifications on how to pass the key value as a prop here and here
Yet, nothing works.
Here's my code:
import React from "react";
const TocContent = (props) => {
return (
<div className="">
{props.TOC.map((header) => (
<ul
key={header.objectId}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) =>
props.handleHeaderClick(
header.level,
header.treepath,
header.containsLaw,
header.sections,
header.secNum,
header.objectId,
header.id,
e.stopPropagation(),
)
}
className="TOC TOCsection"
>
{header._id}
{props.headerIndex === header.objectId
? props.headers2.map((node2) => (
<HeaderList
key={node2.objectId}
header={node2}
props={props}
keyId={node2.objectId}
>
{console.log(props.keyId)}
//--problem is here-- {props.headerIndex2 === props.keyId
? props.headers3.map((node3) => (
<HeaderList
key={node3.objectId}
header={node3}
props={props}
>
{props.headerIndex3 === node3.objectId
? props.headers4.map((node4) => (
<HeaderList
header={node4}
key={node4.objectId}
props={props}
/>
))
: null}
</HeaderList>
))
: null}
</HeaderList>
))
: null}
</ul>
))}
</div>
);
};
const HeaderList = ({ header, props }) => {
return (
<ul
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) =>
props.handleHeaderClick(
header.level,
header.treepath,
header.containsLaw,
header.sections,
header.secNum,
header.objectId,
header.id,
e.stopPropagation(),
)
}
>
{header._id}
</ul>
);
};
export default TocContent;
I finally resorted to change the structure a bit. Instead of the code above, I opted to render the HeaderList component directly in its own component (as a child of itself). This way, I'm able to read header.objectId and make the code shorter.
Here's the new code:
import React from "react";
const TocContent = (props) => {
return (
<div className="">
{props.TOC.map((header) => (
<HeaderList key={header.objectId} header={header} props={props} />
))}
</div>
);
};
const HeaderList = ({ header, props }) => {
return (
<ul
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) =>
props.handleHeaderClick(
header.level,
header.treepath,
header.containsLaw,
header.sections,
header.secNum,
header.objectId,
header.id,
e.stopPropagation(),
)
}
>
{header._id}
{/* // if savedIndex === CurrentParent Index */}
{props.headerIndex === header.objectId &&
props.headers2.map((node2) => (
<HeaderList key={node2.objectId} header={node2} props={props} />
))}
{props.headerIndex2 === header.objectId &&
props.headers3.map((node3) => (
<HeaderList key={node3.objectId} header={node3} props={props} />
))}
{props.headerIndex3 === header.objectId &&
props.headers4.map((node4) => (
<HeaderList header={node4} key={node4.objectId} props={props} />
))}
</ul>
);
};
export default TocContent;
I understand this is maybe not the cleanest code, but an improvement nonetheless. If someone wants to propose something better, it will be much appreciated.

Combine API results from multiple arrays into a single array in a React.js component

I have the following React.js component where I get 10 multiple-choice trivia questions via an API call using fetch and recursively list them on the page via nested components.
The API provides 'correct_answer' as string, and 'incorrect_answers' separately as an array of strings. In my current code, I am only able to list the 'correct' and 'incorrect' answers in their own components.
What I would like to do is combine the 'correct' and 'incorrect' answers into a single array and then randomise the output of them, so that the correct answer is not always in the same place in the list. How would I alter my current code to that? I am an absolute beginner at React.js so any pointers are welcome, thanks.
import React, { Component } from "react";
class QuestionContainer extends Component {
constructor() {
super();
this.state = {
questions: []
};
}
componentWillMount() {
const RenderHTMLQuestion = (props) => (<p dangerouslySetInnerHTML={{__html:props.HTML}}></p>)
const RenderHTMLAnswer = (props) => (<li dangerouslySetInnerHTML={{__html:props.HTML}}></li>)
fetch('https://opentdb.com/api.php?amount=10&category=9&type=multiple')
.then(results => {
return results.json();
}).then(data => {
let questions = data.results.map((question, index) => {
return(
<div key={index} className="questionWrapper">
<div className="question" key={question.question}>
<RenderHTMLQuestion HTML={question.question} />
</div>
<ul className="answers">
<RenderHTMLAnswer key={question.correct_answer} HTML={question.correct_answer} />
{question.incorrect_answers.map((answer, index) => (
<RenderHTMLAnswer key={index} HTML={answer} />
))}
</ul>
</div>
)
})
this.setState({questions: questions});
})
}
render() {
return (
<div className="container2">
{this.state.questions}
</div>
)
}
}
export default QuestionContainer;
You can try this,
let questions = data.results.map((question, index) => {
let correctAnswer = false
return (
<div key={index} className="questionWrapper">
<div className="question" key={question.question}>
<RenderHTMLQuestion HTML={question.question} />
</div>
<ul className="answers">
{question.incorrect_answers.map((answer, index) => {
if(Math.floor(Math.random() * Math.floor(question.incorrect_answers.length-1)) === index && !correctAnswer) {
correctAnswer = true
return <> <RenderHTMLAnswer key={question.correct_answer} HTML={question.correct_answer} /> <RenderHTMLAnswer key={index} HTML={answer} /> </>
}
return <RenderHTMLAnswer key={index} HTML={answer} />
})}
</ul>
</div>
)
})
Math.random()
Note: Don't use componentWillMount (UNSAFE), instead you can go for componentDidMount for your API call.
Update
You can also try this,
let questions = data.results.map((question, index) => {
question.incorrect_answers.splice(Math.floor(Math.random() * Math.floor(question.incorrect_answers.length + 1)), 0, question.correct_answer)
return (
<div key={index} className="questionWrapper">
<div className="question" key={question.question}>
<RenderHTMLQuestion HTML={question.question} />
</div>
<ul className="answers">
{question.incorrect_answers.map((answer, index) => {
return <RenderHTMLAnswer key={index} HTML={answer} />
})}
</ul>
</div>
)
})
Array.prototype.splice()
Demo
You can try using lodash
https://lodash.com/docs/4.17.15#shuffle
const answers = _.shuffle(_.concat(correct_answers, incorrect_answers));
return (
<ul className="answers">
{answers.map((answer, index) => (<RenderHTMLAnswer key={index} HTML={answer} />))}
</ul>
)

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