react.js error handling inside functional component - reactjs

I have a component which receives a list of items through props.
It looks like this:
const component = (props) => {
return (
<ul>
{props.list.map((item) => (
<ListItem key={item.Id} title={item.title} imgSrc={item.img.url} />
))}
</ul>
);
};
edit:
and the child looks like this:
const ListItem = (props) => {
return (
<li key={props.key}>
<h4>{props.title}</h4>
<div>
<img src={props.imgSrc} alt='thumbnail'
/>
</div>
</li>
);
};
The list comes from an API and there are cases in which the values I am assigning will be undefined or not available (imgSrc for example). This breaks the entire rendering of the app.
How can I handle errors in a way that will skip the problematic item and continue with the mapping? It usually means this is a deleted item so I wish to skip it all together.
I usually wrap the code with a try-catch or if statement but I am not allowed to do it here.

There are many options to solve that. For example, you could use the filter method before your .map call.
const component = (props) => {
return (
<ul>
{props.list.filter((item) => item.img.url !== undefined).map((item) => (
<ListItem key={item.Id} title={item.title} imgSrc={item.img.url} />
))}
</ul>
);
};
Another possible option could be Error Boundaries. I don't think that they are what you need, but it could be interesting for you anyways.

You can conditional rendering.
Array.isArray(props.list) &&
props.list.map((item) => (
<ListItem key={item.Id} title={item.title} imgSrc={item.img.url} />
));
You can only map over the array if it is an array as:
const component = (props) => {
return (
<ul>
{Array.isArray(props.list) && props.list.map((item) => (
<ListItem key={item.Id} title={item.title} imgSrc={item.img.url} />
))}
</ul>
);
};

Related

Why doesn't `index` qualify as a unique key, when passed as part of props to a custom react component?

When I just list the items directly, using index works. As in the following.
<ol className="item-list">
{
props.items.map((item, index) => (
<li key={index}>{item}</li>
))
}
</ol>
But when I create a custom component to represent the list item, using index doesn't seem to qualify as being unique... And I end up getting a warning, as in the following.
<ol className="item-list">
{
props.items.map((item, index) => (
<ShoppingItem
index={index}
item={item}
/>
))
}
</ol>
The ShoppingItem is a simple component, like the following.
const ShoppingItem = props => (
<li key={props.index}>{props.item}</li>
);
And the warning I get in the console is the following.
Warning: Each child in a list should have a unique "key" prop.
You should read carefully the react docs for Lists and Keys: Extracting components with keys. The key goes on the component being mapped, not what it renders.
Incorrect
const ShoppingItem = props => (
<li key={props.index}>{props.item}</li>
);
<ol className="item-list">
{
props.items.map((item, index) => (
<ShoppingItem
index={index}
item={item}
/>
))
}
</ol>
Correct
<ol className="item-list">
{
props.items.map((item, index) => (
<ShoppingItem
key={index} // <-- key goes here
item={item}
/>
))
}
</ol>

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

ReactJS - read json value, first level values are readable but, second level showing error

This is code
const UserItem = (user, index) => (
<div key={index} className="accordion__item js-accordion-item">
<div className="accordion-header js-accordion-header">{(index+1)} . {user.invoiceId}
</div>
<div className="accordion-body js-accordion-body">
<div className="accordion-body__contents">
{user.sender.city}
<button href="#" onClick={handleClick} id={user.invoiceId}>
Click me
</button>
</div>
</div>
</div>
);
You probably used your component UserItem like this in your code
{
users.map((user, index) => <UserItem user={user} index={index} />)
}
In this case you must declare your component like this
const UserItem = ({user, index}) => ( ... );
instead of
const UserItem = (user, index) => ( ... );
Finally you should set the key property on UserItem, not on the div inside UserItem so basically you shoud write
{
users.map((user, index) => <UserItem user={user} index={index} key={index} />)
}
or event better with a unique id
{
users.map((user, index) => <UserItem user={user} index={index} key={user.invoiceId}/>)
}

Calling an External Function in a React Component

I have a need, in a site I'm building, for a list component that is reused several times. However, the list is purely for rendering and is not responsible for the state of the app at all. I know you either cannot, or are not supposed to have dumb components containing any logic, but I am not sure how to proceed without using a smart component, which is entirely unnecessary. Here is my smart component that works:
class Menu extends Component {
renderItems(items) {
return this.props.items.map((i, index) => {
return (
<li key={index} style={{marginLeft: 10}}>
{i}
</li>
)
});
}
render() {
const { listStyle } = styles;
return (
<div>
<ul style={listStyle}>
{this.renderItems()}
</ul>
</div>
)
}
}
And I've tried this:
function Menu(props) {
return props.items.map((i, index) => {
<li key={index} style={{marginLeft: 10}}>
{i}
</li>
});
}
And then calling it inside Nav like this, which does not throw an error but does not render anything from menu either:
const Nav = () => {
const { listStyle, containerStyle } = styles;
return (
<div style={containerStyle}>
<Logo url={'#'}
src={PickAPlayLogo}
width={300} />
<Menu items={pageLinks} />
<Menu items={socialMediaLinks} />
<Logo url={'#'}
src={AppStoreLogo}
width={170} />
</div>
);
};
Also, worth noting, I have never come across a function that is supposed to be rendered like a component, but was trying it based on the example on this page
Heres an answer similar to what you have going on
function Menu(props) {
this.renderItems = () => {
return (
<ul>
{props.items.map((i, index) => {
return (
<li>{i}</li>
)
})}
</ul
)
}
return(
this.renderItems()
)
}
Here we go:
function Menu(props) {
const {listStyle} = styles;
const listItems = props.items.map((i, index) =>
<li key={index} style={{marginLeft: 10}}>
{i}
</li>
);
return (
<ul style={listStyle}>{listItems}</ul>
);
}

Resources