Dealing with an empty array when using .map() in React - reactjs

I have a React.JS component that will map the notes variable to display.
However, I have run into the problem of having no notes and receiving an error. What is a proper way to approach this?
Here is the code:
import React, {Component} from 'react';
class List extends Component {
constructor(props){
super(props);
}
render(){
var notes = this.props.items.map((item, i)=>{
return(
<li className="listLink" key={i}>
<p>{item.title}</p>
<span>{item.content}</span>
</li>
)
});
return(
<div className='list'>
{notes}
</div>
);
}
}
export default List;

If you want to render the notes when at least one note exists and a default view when there are no notes in the array, you can change your render function's return expression to this:
return(
<div className='list'>
{notes.length ? notes : <p>Default Markup</p>}
</div>
);
Since empty arrays in JavaScript are truthy, you need to check the array's length and not just the boolean value of an array.
Note that if your items prop is ever null, that would cause an exception because you'd be calling map on a null value. In this case, I'd recommend using Facebook's prop-types library to set items to an empty array by default. That way, if items doesn't get set, the component won't break.

here is the simplest way to deal with
import React, {Component} from 'react';
class List extends Component {
constructor(props){
super(props);
}
render(){
var notes = this.props.items?.map((item, i)=>{
return(
<li className="listLink" key={i}>
<p>{item.title}</p>
<span>{item.content}</span>
</li>
)
});
return(
<div className='list'>
{notes}
</div>
);
}
}
export default List;
just try to add "?" for the array that you maped

You can just setting a fallback value for the this.props.item
render() {
const items = this.props.items || [] // if there's no items, give empty array
const notes = items.map((item, i) => {
return(
....
Doing .map() on empty array will not produces an error, but will return an empty array. Which is fine because empty array is a renderable item in react and won't produce error in render() and will not render the notes as there are no notes provided.

In your component, you can set the defaultProps property to set the initial value to your props:
static defaultProps = {
items: []
};

Related

react recreating a component when I don't want to

I'm super new to react, this is probably a terrible question but I'm unable to google the answer correctly.
I have a component (CogSelector) that renders the following
import React from "react"
import PropTypes from "prop-types"
import Collapsible from 'react-collapsible'
import Cog from './cog.js'
const autoBind = require("auto-bind")
import isResultOk from "./is-result-ok.js"
class CogSelector extends React.Component {
constructor(props) {
super(props)
this.state = {
docs: null,
loaded: false,
error: null
}
autoBind(this)
}
static get propTypes() {
return {
selectCog: PropTypes.func
}
}
shouldComponentUpdate(nextProps, nextState){
if (nextState.loaded === this.state.loaded){
return false;
} else {
return true;
}
}
componentDidMount() {
fetch("/api/docs")
.then(isResultOk)
.then(res => res.json())
.then(res => {
this.setState({docs: res.docs, loaded: true})
}, error => {
this.setState({loaded: true, error: JSON.parse(error.message)})
})
}
render() {
const { docs, loaded, error } = this.state
const { selectCog } = this.props
if(!loaded) {
return (
<div>Loading. Please wait...</div>
)
}
if(error) {
console.log(error)
return (
<div>Something broke</div>
)
}
return (
<>
Cogs:
<ul>
{docs.map((cog,index) => {
return (
<li key={index}>
<Cog name={cog.name} documentation={cog.documentation} commands={cog.commands} selectDoc={selectCog} onTriggerOpening={() => selectCog(cog)}></Cog>
</li>
// <li><Collapsible onTriggerOpening={() => selectCog(cog)} onTriggerClosing={() => selectCog(null)} trigger={cog.name}>
// {cog.documentation}
// </Collapsible>
// </li>
)
})}
{/* {docs.map((cog, index) => { */}
{/* return ( */}
{/* <li key={index}><a onClick={() => selectCog(cog)}>{cog.name}</a></li>
)
// })} */}
</ul>
</>
)
}
}
export default CogSelector
the collapsible begins to open on clicking, then it calls the selectCog function which tells it's parent that a cog has been selected, which causes the parent to rerender which causes the following code to run
class DocumentDisplayer extends React.Component{
constructor(props) {
super(props)
this.state = {
cog: null
}
autoBind(this)
}
selectCog(cog) {
this.setState({cog})
}
render(){
const { cog } = this.state
const cogSelector = (
<CogSelector selectCog={this.selectCog}/>
)
if(!cog) {
return cogSelector
}
return (
<>
<div>
{cogSelector}
</div>
<div>
{cog.name} Documentation
</div>
<div
dangerouslySetInnerHTML={{__html: cog.documentation}}>
</div>
</>
)
}
}
export default DocumentDisplayer
hence the cogSelector is rerendered, and it is no longer collapsed. I can then click it again, and it properly opens because selectCog doesn't cause a rerender.
I'm pretty sure this is just some horrible design flaw, but I would like my parent component to rerender without having to rerender the cogSelector. especially because they don't take any state from the parent. Can someone point me to a tutorial or documentation that explains this type of thing?
Assuming that Collapsible is a stateful component that is open by default I guess that the problem is that you use your component as a variable instead of converting it into an actual component ({cogSelector} instead of <CogSelector />).
The problem with this approach is that it inevitably leads to Collapsible 's inner state loss because React has absolutely no way to know that cogSelector from the previous render is the same as cogSelector of the current render (actually React is unaware of cogSelector variable existence, and if this variable is re-declared on each render, React sees its output as a bunch of brand new components on each render).
Solution: convert cogSelector to a proper separated component & use it as <CogSelector />.
I've recently published an article that goes into details of this topic.
UPD:
After you expanded code snippets I noticed that another problem is coming from the fact that you use cogSelector 2 times in your code which yields 2 independent CogSelector components. Each of these 2 is reset when parent state is updated.
I believe, the best thing you can do (and what you implicitly try to do) is to lift the state up and let the parent component have full control over all aspects of the state.
I solved this using contexts. Not sure if this is good practice but it certainly worked
render() {
return (
<DocContext.Provider value={this.state}>{
<>
<div>
<CogSelector />
</div>
{/*here is where we consume the doc which is set by other consumers using updateDoc */}
<DocContext.Consumer>{({ doc }) => (
<>
<div>
Documentation for {doc.name}
</div>
<pre>
{doc.documentation}
</pre>
</>
)}
</DocContext.Consumer>
</>
}
</DocContext.Provider>
)
}
then inside the CogSelector you have something like this
render() {
const { name, commands } = this.props
const cog = this.props
return (
//We want to update the context object by using the updateDoc function of the context any time the documentation changes
<DocContext.Consumer>
{({ updateDoc }) => (
<Collapsible
trigger={name}
onTriggerOpening={() => updateDoc(cog)}
onTriggerClosing={() => updateDoc(defaultDoc)}>
Commands:
<ul>
{commands.map((command, index) => {
return (
<li key={index}>
<Command {...command} />
</li>
)
}
)}
</ul>
</Collapsible>
)}
</DocContext.Consumer>
)
}
in this case it causes doc to be set to what cog was which is a thing that has a name and documentation, which gets displayed. All of this without ever causing the CogSelector to be rerendered.
As per the reconciliation algorithm described here https://reactjs.org/docs/reconciliation.html.
In your parent you have first rendered <CogSelector .../> but later when the state is changed it wants to render <div> <CogSelector .../></div>... which is a completely new tree so react will create a new CogSelector the second time

How do I render a nested Array with React.js?

I am new to react.js and I have stumbled upon a subject I can't wrap my head around. I have a long array of items which I want to display in two different rows, which is why I take chunks out of this array and add them to a nested array without key and values like so:
constructor(props) {
super(props)
this.state = {
characters: [["Anakin","Darth Vader","Snoke"], ["Darth Maul", "Yoda", "Luke Skywalker"]],
}
}
In the render function of the "Characters" component I use the map function and what I want is each array to be passed to the component "Subarray":
Characters component
render() {
return (
<div>
{this.state.characters.map((item, index )=> <Subarray key={index} title={item}></Subarray>)}
</div>
)
}
And then in the "Subarray" component I want to add a html element to every element in that array:
Subarray component
export class Subarray extends Component {
render() {
return (
<div>
{this.props.title}
</div>
)
}
}
I can't get each element of the array to be wrapped within a div element:
Output:
<div><div>AnakinDarth VaderSnoke</div><div>Darth MaulYodaLuke Skywalker</div></div>
You can do this, assuming this.props.title contains ["Anakin","Darth Vader","Snoke"] :
export class Subarray extends Component {
render() {
return (
<div>
{this.props.title.map((each, index) => <div key={index}>{each}</div>)}
</div>
)
}
}
I think that you have to change Subarray to be something like
export class Subarray extends Component {
render() {
return (
<div>
{this.props.title.map((item, index) => {
return <div key={index}>{item}</div>
})}
</div>
)
}
}
in this way you loop through each item in the subarray and render every one of them

Is it possible to create a generic React list component?

I'm just getting started with React and ES6 and I am trying to DRY up my app a bit. What I'm looking to do is create a component that takes a collection and an attribute of the items in that collection and turn it into a list. For example, if I pass in a group of authors and specify last name, it will create a list of the authors' last names; using the same component I would like to use it elsewhere and pass in a group of songs and list them out by title.
Here's what I have so far:
ItemList:
import PropTypes from 'prop-types'
import React from 'react'
import Item from './Item'
export default class ItemList extends React.Component {
constructor(props){
super(props)
}
render() {
let items
if(this.props.items.length === 0){
items = <div>Nothing Found</div>
} else {
items = this.props.items.map(item => {
return(
<Item item={item} displayAttribute={this.props.displayAttribute}
)
});
}
return (
<div className="item-index">
<div className="list-group">{items}</div>
</div>
);
}
}
Item:
import PropTypes from 'prop-types'
import React from 'react'
export default class Item extends React.Component {
constructor(props){
super(props)
}
render() {
return (
<div className="list-group-item" data-index={this.props.item.id}>
<div className="item-attribute">
*This is where I want to print the item's display attribute*
</div>
</div>
);
}
}
Elsewhere in the app, I would like to be able to call something like the following:
<ItemList items={this.state.authors} displayAttribute="last_name" />
or
<ItemList items={this.state.songs} displayAttribute="title" />
and the component would create a list using the specified attribute
If I understood you correctly this should do (in your item list):
<div className="item-attribute">
{this.props.item[this.props.displayAttribute]}
</div>

Component render triggered, but DOM not updated

I'm having problems with my first React application.
In practice, I have a hierarchy of components (I'm creating a multimedia film gallery) which, upon clicking on a tab (represented by the Movie component) must show the specific description of the single film (SingleMovieDetails).
The problem is that the DOM is updated only on the first click, then even if the SingleMovieDetails props change, the DOM remains locked on the first rendered movie.
Here's the code i wrote so far...
//Movie component
import React from "react";
import styles from "./Movie.module.scss";
import PropTypes from "prop-types";
class Movie extends React.Component{
constructor(props){
super(props);
this.imgUrl = `http://image.tmdb.org/t/p/w342/${this.props.movie.poster_path}`;
}
render(){
if(!this.props.size)
return <div onClick={this.props.callbackClick(this.props.movie.id)}
name={this.props.movie.id}
className={styles.movieDiv}
style={{backgroundImage: `url(${this.imgUrl})`}}></div>;
return <div onClick={() => this.props.callbackClick(this.props.movie.id)}
name={this.props.movie.id}
className={styles.movieDivBig}
style={{backgroundImage: `url(${this.imgUrl})`}}></div>;
}
}
Movie.propTypes = {
movie: PropTypes.any,
callbackClick: PropTypes.any
};
export default Movie;
SingleMovieDetails.js
import React from "react";
import styles from "./SingleMovieDetails.module.scss";
import Movie from "../Movie";
import SingleMovieDescription from "../SingleMovieDescription";
import MovieCast from "../MovieCast";
import SingleMovieRatings from "../SingleMovieRatings";
class SingleMovieDetails extends React.Component{
constructor(props){
super(props);
console.log(props);
this.state = props;
console.log('constructor', this.state.movie)
}
render(){
console.log('SMD', this.state.movie)
return (
<>
<div className={styles.container}>
<div className={styles.flayer}>
<Movie size={'big'} movie={this.state.movie}/>
</div>
<div className={styles.description}>
<SingleMovieDescription movie={this.state.movie}/>
<MovieCast></MovieCast>
</div>
<div className={styles.ratings}>
<SingleMovieRatings />
</div>
</div>
</>
);
}
}
export default SingleMovieDetails;
MovieCarousel.js
import React from "react";
import PropTypes from "prop-types";
import Movie from "../Movie";
import styles from "./MovieCarousel.module.scss";
import SingleMovieDetails from "../SingleMovieDetails";
class MovieCarousel extends React.Component {
constructor(props) {
super(props);
this.state = [];
this.callbackClickMovie = this.callbackClickMovie.bind(this);
}
callbackClickMovie(id) {
const singleMovieApi = `https://api.themoviedb.org/3/movie/${id}?api_key=b6f2e7712e00a84c50b1172d26c72fe9`;
fetch(singleMovieApi)
.then(function(response) {
return response.json();
})
.then(data => {
this.setState({ selected: data });
});
}
render() {
let details = null;
if (this.state.selected) {
details = <SingleMovieDetails movie={this.state.selected} />;
}
let counter = 6;
let movies = this.props.movies.map(el => {
let element = (
<Movie movie={el} callbackClick={this.callbackClickMovie} />
);
counter--;
if (counter >= 0) return element;
return;
});
let content = (
<>
<h2 className={styles.carouselTitle}>{this.props.title}</h2>
{movies}
{details}
</>
);
return content;
}
}
MovieCarousel.propTypes = {
children: PropTypes.any
};
export default MovieCarousel;
I would be really grateful if someone could help me. I have been on it for two days but I can't really deal with it
This is because in SingleMovieDetails component, you are storing the props values in state and not updating the state on props change. constructor will not get called again so state will always have the initial values.
You have two options to solve the issue:
Directly use the props values instead of storing data in state (preferred way). Like this:
class SingleMovieDetails extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<>
<div className={styles.container}>
<div className={styles.flayer}>
<Movie size={'big'} movie={this.props.movie}/>
</div>
<div className={styles.description}>
<SingleMovieDescription movie={this.props.movie}/>
<MovieCast></MovieCast>
</div>
<div className={styles.ratings}>
<SingleMovieRatings />
</div>
</div>
</>
);
}
}
Use getDerivedStateFromProps, and update the state value on props change.
Same issue with Movie component also, put this line in the render method, otherwise it will always show same image:
const imgUrl = `http://image.tmdb.org/t/p/w342/${this.props.movie.poster_path}`
And use this imgUrl variable.
your Problem is just related to one file: SingleMovieDetails.js
Inside the constructor you´re setting the component state to get initialized with the props (send to the component the first time)
But inside your render() method you are referencing that state again:
<Movie size={'big'} movie={this.state.movie}/>
All in all thats not completely wrong, but you need to do one of two things:
Add a method to update your component state with the nextPropsReceived (Lifecycle Method was called: will receive props, if you are using the latest version you should use: getDerivedStateFromProps)
preferred option: you dont need a state for the movie component, so just use the props inside the render function (this.props.movie)
afterwards you can also delete the constructor, because there is nothing special inside. :)
edit:
So, just to be clear here: Since you´re only setting the state once (the constructor is not called on every lifecycle update) you will always only have the first value saved. Changing props from outside will just trigger render(), but wont start the constructor again ;D

react mapping over array of objects

My current state is an array of objects. I am mapping over them and I am getting exactly what I want. However, inside of my array of objects I am not receiving the ingredients that I want. I am receiving the console.log of that value but the value it self I am not displaying anything on the dom. Heres my code. I am trying to have my ingredients show up inside of the li that I am mapping but when I click on my panels I am receiving no value. However, my console.log below it shows a value. idk...
import React from 'react';
import Accordian from 'react-bootstrap/lib/Accordion';
import Panel from 'react-bootstrap/lib/Panel';
import Button from 'react-bootstrap/lib/Button';
import ButtonToolbar from 'react-bootstrap/lib/ButtonToolbar';
import Modal from 'react-bootstrap/lib/Modal';
import FormGroup from 'react-bootstrap/lib/FormGroup';
import ControlLabel from 'react-bootstrap/lib/ControlLabel';
import FormControl from 'react-bootstrap/lib/FormControl';
export default class App extends React.Component {
constructor () {
super();
this.state = {recipes: [
{recipeName: 'Pizza', ingredients: ['Dough', 'Cheese', 'Sauce']},
{recipeName: 'Chicken', ingredients: ['Meat', 'Seasoning', 'other']},
{recipeName: 'Other', ingredients: ['other1', 'other2', 'other3']}
]};
console.log(this.state.recipes);
}
render() {
const recipes = this.state.recipes.map((recipe, index) => {
return (
<Panel header={recipe.recipeName} eventKey={index} key={index}>
<ol>
{recipe.ingredients.map((ingredient) => {
<li> {ingredient} </li>
console.log(ingredient);
})}
</ol>
</Panel>
)
});
return (
<div className="App container">
<Accordian>
{recipes}
</Accordian>
</div>
)
}
}
Because you are not returning anything from inner map body.
Write it like this:
{recipe.ingredients.map((ingredient) => {
console.log(ingredient);
return <li key={...}> {ingredient} </li> //use return here
})}
Or you can simply write it like this:
{
recipe.ingredients.map((ingredient) => <li key={...}> {ingredient} </li>)
}
As per MDN Doc:
Arrow functions can have either a "concise body" or the usual "block
body".
In a concise body, only an expression is needed, and an implicit
return is attached. In a block body, you must use an explicit return
statement.
Check MDN Doc for more details about Arrow Function.

Resources