Setting state with async value - reactjs

I'm trying to fetch data for a React component and set it as a nested object in state:
import React from 'react';
import './App.css';
import XLSX from "xlsx";
class App extends React.Component {
constructor(props){
super(props);
this.state = {
isLoading:false,
cards:{}
}
}
componentDidMount(){
this.setState({isLoading:true});
/* convert xlsx to JSON */
fetch("../data/Cards_v0.1.xlsx")
.then((d)=> d.arrayBuffer()).then((d)=>{
const data = new Uint8Array(d);
const workbook = XLSX.read(data, {type:"buffer"});
const sheet = workbook.Sheets["Deck"];
const cardsJSON = XLSX.utils.sheet_to_json(sheet,{range:1});
let cards = {};
for(let i=0; i<cardsJSON.length; i++){
for(let j=0; j<cardsJSON.length; j++){
cards[cardsJSON[i].Name] = cardsJSON[i];
}
}
this.setState({cards:cards, isLoading:false});
});
}
render() {
if(this.state.isLoading){
return <div>Loading</div>;
}
return (
<div>
{ this.state.cards.Single.Name }
</div>
);
}
}
export default App;
React devtools shows that the object is in state, with cards>Single>Name being "Single", but {this.state.cards.Single.Name} throws TypeError: Cannot read property 'Name' of undefined.
What's confusing me most is that {this.state.cards.Single} instead throws Objects are not valid as a React child (found: object with keys {Name, Type, Rarity, Text, Money}). If you meant to render a collection of children, use an array instead.
So the key Name is found when it's not being called, but then the object becomes undefined when I call it?
Very confused. Any help is appreciated!

React doesn't know how to display objects, therefore, {this.state.cards.Single} will throw Objects are not valid as a React child.
You also have some odd choice of setting React state. Since the component is always going to fetch data on mount, it makes more sense to make isLoading to be defaulted to true, then set to false on successful fetch response.
I don't know how your cardsJSON is structured, but the example below shows two ways to display nested JSON.
Wrapping it in pre, code html elements and using JSON.stringify(obj, replacer, spaces)
Destructing the object properties from this.state.cards (if any of these are properties that are also nested objects, then they'll also need to be destructed as well!) and then displaying all destructed data in a table, list, etc.
Working example: https://codesandbox.io/s/km3wwvqqzv
Example.js
import React, { Component, Fragment } from "react";
import DisplayCode from "./displayCode";
import DisplayList from "./displayList";
export default class Example extends Component {
state = {
isLoading: true,
cards: {}
};
componentDidMount = () => {
fetch("https://jsonplaceholder.typicode.com/users")
.then(response => response.json())
.then(json => this.setState({ cards: json, isLoading: false }));
};
render = () =>
this.state.isLoading ? (
<div>Loading</div>
) : (
<Fragment>
<DisplayCode cards={this.state.cards} />
<DisplayList cards={this.state.cards} />
</Fragment>
);
}
displayCode.js
import React, { Fragment } from "react";
export default ({ cards }) => (
<Fragment>
<h3>Display JSON as code:</h3>
<pre style={{ height: 300, overflowY: "auto" }}>
<code>{JSON.stringify(cards, null, 4)}</code>
</pre>
</Fragment>
);
displayList.js
import map from "lodash/map";
import React, { Fragment } from "react";
export default ({ cards }) => (
<Fragment>
<h3 style={{ marginTop: 30 }}>Display JSON as list:</h3>
<ul style={{ height: 300, overflowY: "auto" }}>
{map(
cards,
({
id,
name,
username,
email,
address: {
street,
suite,
city,
zipcode,
geo: { lat, lng }
}
}) => (
<li key={id}>
<strong>id:</strong> {id}
<ul>
<li>
<strong>Username:</strong> {username}
</li>
<li>
<strong>Name:</strong> {name}
</li>
<li>
<strong>Email:</strong> {email}
</li>
<li>
<strong>Street: </strong>
{street}
</li>
<li>
<strong>Suite: </strong>
{suite}
</li>
<li>
<strong>City: </strong>
{city}
</li>
<li>
<strong>Zipcode: </strong>
{zipcode}
</li>
<li>
<strong>Lat: </strong>
{lat}
</li>
<li>
<strong>Lng: </strong>
{lng}
</li>
</ul>
</li>
)
)}
</ul>
</Fragment>
);

Related

How to access function from different components React

Here's the code for Panel
`
import React from "react";
// import {render} from "react-dom";
import AddInventory from "components/AddInventory";
class Panel extends React.Component{
constructor(props) {
super(props);
this.state = {
activeIndex: ''
}
}
componentDidMount() {
this.activePanel();
}
closePanel=()=>{
this.setState({
activeIndex : false
})
}
activePanel = ()=>{
this.setState({
activeIndex : true
})
}
render(){
return(
<div>
{/*<button className={"button is-primary add-btn"} onClick={this.activePanel}>add</button>*/}
<div className={this.state.activeIndex ? 'panel-wrapper active':'panel-wrapper'}>
<div className={"over-layer"}>
<div className={"panel"}>
<div className={"head"}>
<span onClick={this.closePanel} className={"close"}>x</span>
<AddInventory></AddInventory>
</div>
</div>
</div>
</div>
</div>
)
}
}
export default Panel;
Products:
import React from "react";
import ToolBox from "components/ToolBox";
import Product from "components/Product";
import axios from 'components/axios'
import {CSSTransition , TransitionGroup} from 'react-transition-group'
import Panel from "components/Panel";
class Products extends React.Component{
product =[];
source =[];
state ={
product : [{
id:'1',
name:'Air Jordan1',
tags:'45 colours',
image:'images/1.jpg',
price:'21000',
status:'available'
},
{
id:'2',
name:'Nike Pual George PG 3',
tags:'45 colours',
image:'images/2.jpg',
price:'11000',
status:'available'
},
{
id:'3',
name:'Jordan Why Not Zer0.2',
tags:'10 colours',
image:'images/3.jpg',
price:'15000',
status:'unavailable'
},
]
}
componentDidMount() {
// fetch('http://localhost:3003/products').then(response => response.json()).then( data=>{
// console.log(data)
// this.setState({
// product : data
// })
// })
axios.get('/products').then(response => {
this.setState( {
product : response.data,
source : response.data
})
})
}
search = text=>{
//1.get a new array from product
let _product = [...this.state.source]
//2.filter the array
let res = _product.filter((element)=>{
return element.name.toLowerCase().includes(text.toLowerCase())
})
//set state
this.setState({
product : res
})
}
add = ()=>{
let panel = new Panel(this.props)
panel.activePanel()
}
// add =()=>{
// panel.setState({
// activeIndex : true
// })
// }
render() {
return(
<div>
<ToolBox search={this.search}/>
<div className={'products'}>
<div className="columns is-multiline is-desktop">
<TransitionGroup component={null}>
{
this.state.product.map(p=>{
return (
<CSSTransition
timeout={400}
classNames="product-fade"
key={p.id}
>
<div className="column is-3" key={p.id}>
<Product product={p}/>
</div>
</CSSTransition>
)
})
}</TransitionGroup>
{/*<div className="column is-3">*/}
{/* <Product/>*/}
{/*</div>*/}
{/*<div className="column is-3">*/}
{/* <Product/>*/}
{/*</div>*/}
</div>
<button className={"button is-primary add-btn"} onClick={this.add}></button>
</div>
</div>
)
}
}
export default Products;
I was trynna use activePanel() in Products but it gives me : Warning: Can't call setState on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign tothis.statedirectly or define astate = {};` class property with the desired state in the Panel component.
I tried initialize a new panel() but it still gives me the same error.
welcome. I don't think this approach is best practice. Generally, components should only ever be updating their own state (see here) and typically you want data to flow from parent component to child component (see here). Additionally, your design is deceptive. When you render a component, you declare it as JSX in some render (or return) statement. But here, Panel is never formally instantiated in JSX.
In Panel, I would suggest watching a prop such as active via shouldComponentUpdate and updating state based on changes to that prop. Then in Products you can instantiate an instance of Panel in JSX and dynamically set the value of that prop.

Updating one value in an object of arrays

I have an array containing objects with details for an image (URL, description, likes) I'm trying to clone an Instagram page, and update the "likes" for that 1 image on click.
Attempted to map through the array and return with the "likes" + 1.
Here are 3 separate files starting with the data. The data is stored in the "Main" section in the state Gallery. So to overview, I want to increase the number of likes when I click on that image. But when I setState, I have no idea how I can only target one value in one object of the array. I would rather just update the state rather than create a new state onClick and then change the value that was! I'm looking for the best practice. (as this is the only way I can learn) Thanks in advance.
const images =[
{
url:'./images/img1.jpg',
description:"test1",
likes:0,
index:0
},
{
url:'./images/img2.jpg',
description:"test1",
likes:3,
index:1
},
{
url:'./images/img3.jpg',
description:"test1",
likes:4,
index:2
},
{
url:'./images/img2.jpg',
description:"test1"
},
{
url:'./images/img2.jpg',
description:"test1"
},
{
url:'./images/img2.jpg',
description:"test1"
},
]
export default images
import React from 'react'
const Gallery =(props)=>{
return (
<div className="container">
<div className="main-gallery">
{props.gallery.map((item,index) => (
<div key={index} className='img-container' onClick= {props.increaseLikes}>
<img className='gallery-images' src={item.url}/>
<p className='likes'>likes {item.likes}</p>
</div>
))}
</div>
</div>
)
}
export default Gallery
import React, { Component } from "react";
import ReactDOM from 'react-dom';
import Nav from './Components/Navbar/Nav'
import Header from './Components/Header/Header'
import Carousel from './Components/Carousel/Carousel'
import Data from './Data'
import Gallery from './Components/Gallery/Gallery'
class Main extends Component {
constructor(props) {
super(props);
this.state={
post:100,
gallery:[],
}
}
componentDidMount(){
this.setState({
gallery:Data
})
}
increaseLikes=()=>{
//no idea how to update
}
render() {
return (
<div>
<Gallery gallery={this.state.gallery} increaseLikes= {this.increaseLikes}/>
</div>
)
}
}
export default Main;
Your increaseLikes function needs to get id of the image from the Gallery component.
So the code must be like something like this:
I assumed your data has an unique id property.
increaseLikes = id => {
const updatedData = this.state.gallery.map(image => {
if (image.id === id) {
return { ...image, likes: image.likes ? image.likes + 1 : 1 };
} else {
return image;
}
});
this.setState({
gallery: updatedData
})
};
Gallery component code:
import React from "react";
const Gallery = props => {
return (
<div className="container">
<div className="main-gallery">
{props.gallery.map((item, index) => (
<div
key={item.id}
className="img-container"
onClick={() => props.increaseLikes(item.id)}
>
<img
className="gallery-images"
src={item.url}
alt={item.description}
/>
<p className="likes">likes {item.likes ? item.likes : 0} </p>
<hr />
</div>
))}
</div>
</div>
);
};
export default Gallery;
you could use the url (That seems to be the only unique value) of the images in order to update your array, I've made a StackBlitz where you can see how to do it. Hope this helps.

How to fix: How to show state with onClick to div?(React)

I have sidebar with document types on it(docs, table, slider, html ..). I want that, if i click on docs element it will show docs in another div like a header.
I have 3 files: DocumentType.tsx, Sidebar.tsx and Results.tsx
In DocumentType.tsx:
import React from 'react';
const documentType = (props ) =>{
return(
<div>
<p id="fileType">{props.type}</p>
</div>
)
};
export default documentType;
In Sidebar.tsx:
typeState = {
documentTypes: [
{ type: "Dokumendid" },
{ type: "PDF" },
]
}
toDocument = () => {
this.setState({
documentTypes: [
{ type: "Dokumendid" }
console.log("Document was clicked");
]
})
}
toPdf = () => {
this.setState({
documentTypes: [
{ type: "Pdf" }
console.log("PDF was clicked")
]
})
}
render(){
return(
<a className="a" href="/search?filter%3Atype=doc" onClick={this.toDocument}>
<div className="icons dokument">
<img src={dokument} alt="dokument"/>
<a className="title">dokument</a>
</div>
</a>
<a className="a" href="/search?filter%3Atype=pdf" onClick={this.toPdf}>
<div className="icons pdf">
<img src={pdf} alt="pdf"/>
<a className="title">pdf</a>
</div>
</a>
)
}
And in Results.tsx:
...
<DocumentType />
..
You want to show a document type in Results component when a document in Sidebar component is clicked.
You have documentType state in Sidebar component and you want to pass it to Results component. So for that you can make Results component as child component of Sidebar component and pass the selected document type i.e documentType state as props.
Sidebar.js
import React, {Component} from 'react'
import Results from 'path-to-results';
class Sidebar extends Component {
state = {
// instead of using "documentType" as array
// you can make it null for initial value
documentType: null
}
// instead of using "toPDF" or "toDocument" method
// you can use single method to update the state
handleDocType = (docType) => {
this.setState({
documentType: docType
})
}
render() {
return (
<div>
// pass "document" as argument to handleDocType method
<a className="a" href="#" onClick={() => this.handleDocType('document')}>
<div className="icons dokument" >
<img src="" alt="dokument"/>
<a className="title">dokument</a>
</div>
</a>
// pass "pdf" as argument to handleDocType method
<a className="a" href="#" onClick={() => this.handleDocType('pdf')}>
<div className="icons pdf">
<img src="" alt="pdf"/>
<a className="title">pdf</a>
</div>
</a>
// checking if "documentType" is null or not
// if it is null nothing is rendered
// if it is not null then "Results" component is rendered
{ this.state.documentType && <Results type={this.state.documentType} /> }
</div>
)
}
}
Results.js
import React, { Component } from 'react'
import DocType from 'path-to-doctype'
class Results extends Component {
// .... your other codes
render() {
return (
<div>
// ....... your other codes
<DocType type={this.props.type} />
</div>
)
}
}
export default Results
DocType.js
import React from 'react';
const DocumentType = (props ) =>{
return(
<div>
<p id="fileType">{props.type}</p>
</div>
)
};
export default DocumentType;
UPDATE
If Sidebar and DocType components are children components of Results component then add documentType state to Results component and pass documentType state as props to DocType component.
Results.js
class Results extends Component {
// add state "documentType"
state = {
documentType: null
}
// add "handleDocType" method
handleDocType = (docType) => {
this.setState({
documentType: docType
})
}
// .... your other codes
render() {
return (
<div>
// .... your other codes
// pass "handleDocType" as props to Sidebar component
<Sidebar handleDocType={this.handleDocType}/>
// pass "documentType" state as props to DocType component
<DocType type={this.state.documentType} />
</div>
)
}
}
export default Results
Sidebar.js
class Sidebar extends Component {
// use "docTypeHandler" to call parent "handleDocType" method
// that updates "documentType" state in Results component
docTypeHandler = (doctype) => {
this.props.handleDocType(doctype)
}
render() {
return (
<div>
<a className="a" href="#" onClick={() => this.docTypeHandler('document')}>
<div className="icons dokument" >
<img src="" alt="dokument"/>
<a className="title">dokument</a>
</div>
</a>
<a className="a" href="#" onClick={() => this.docTypeHandler('pdf')}>
<div className="icons pdf">
<img src="" alt="pdf"/>
<a className="title">pdf</a>
</div>
</a>
</div>
)
}
}
export default Sidebar
DocType.js
const DocType = (props ) =>{
return(
<div>
<p id="fileType">{props.type}</p>
</div>
)
};
If I understood your question correctly.. you wanted to show data in a div when onClick event triggers..
lets say your state object has
state = {
data: ''
}
//clicked function
clicked =() => {
this.setState({data: 'clickedme'})
}
div element: <div onClick={this.clicked} >{this.state.data}</div>
simple example when an onClick event occurs a div and displaying the state data object..

React: Limit action to a single component

This is a container:
render() {
return (
<div>
<ul className="list-group" id="contact-list">
{this.returnContactList().map(
(contact) =>
<li key={contact.date.N} className="list-group-item">
<ContactCard contact={contact} onFormSubmit={this.handleSubmit} summaryHidden={this.state.summaryHidden} />
</li>
)}
</ul>
</div>
);
}
This is what I have as a component:
import React from 'react';
import '../Contacts.css';
const ContactCard = ({ contact, onFormSubmit, summaryHidden }) => {
return (
<div>
<button onClick={onFormSubmit}>submit</button>
<div style={{ display: summaryHidden ? 'block' : 'none' }}>
Summary
</div>
</div>
)
}
export default ContactCard;
This renders list of contacts. But when I click button, the text Summary gets applied to all components. I want to limit it to a single component only. How to do that?
You only have one "summaryHidden" variable in your container. You need a separate one for each component (maybe store them on the contact objects in the map) if you want the summaries to be hidden in some components and not others.

react-redux: Rendering a component after an API call

I am building an app which uses user input and shows number of recipes and they can click on recipe card to view ingredients as well. Every time they click on recipe card I make an API call to get appropriate recipe ingredient. But I am not able to figure out how to show the component which contains the recipe ingredients. I tried with conditional routing and conditional rendering as well but couldn't find the solution.
Recipe_Template.js
export class RecipeTemplate extends Component {
renderRecipe = recipeData => {
return recipeData.recipes.map(recipeName => {
return (
<div className="container">
<div className="row">
<div className="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<a
href={recipeName.source_url}
target="_blank"
onClick={() => {
this.props.fetchRecipeId(recipeName.recipe_id);
}}
>
<img
src={recipeName.image_url}
className="mx-auto d-block img-fluid img-thumbnail"
alt={recipeName.title}
/>
<span>
<h3>{recipeName.title}</h3>
</span>
</a>
<span}>
<h3>{recipeName.publisher}</h3>
</span>
</div>
</div>
</div>
);
});
};
render() {
return (
<React.Fragment>
{this.props.recipe.map(this.renderRecipe)}
</React.Fragment>
);
}
}
Recipe_Detail.js
class RecipeDetail extends Component {
renderRecipeDetail(recipeData) {
return recipeData.recipe.ingredients.map(recipeIngredient => {
return <li key={recipeIngredient}>recipeIngredient</li>;
});
}
render() {
if (this.props.recipeId === null) {
return <div>Loading...</div>;
}
return <ul>{this.props.recipeId.map(this.renderRecipeDetail)}</ul>;
}
}
function mapStateToProps({ recipeId }) {
return { recipeId };
}
export default connect(mapStateToProps)(RecipeDetail);
Not entirely sure why you would need Redux here (unless it's being shared among other nested components), but I'm fairly certain you can just utilize React state.
One approach would be to configure your routes as such:
<Route path="/recipes" component={Recipes} />
<Route path="/recipe/:id" component={ShowRecipe} />
When the user sends a query, gets some results, and you display all matching recipes to a Recipes component. Each recipe then has a name (and other associated displayable data) and a clickable link:
<Link to={`/recipe/id?recipeId=${recipeId}`}>View {recipeName} Recipe</Link>
which for simplicity sake might look like:
<ul>
<Link to="/recipe/id?recipeId=08861626">View Prosciutto Bruschetta Recipe</Link>
<Link to="/recipe/id?recipeId=04326743">View Pasta Bundt Loaf Recipe</Link>
...etc
</ul>
When the user clicks on the link, react-router sends the user to the ShowRecipe component with a unique recipeId.
ShowRecipe then makes another AJAX request to get the recipe details:
ShowRecipe.js
export default class ShowRecipe extends Component {
state = { recipeDetail: '' }
componentDidMount = () => {
const { recipeId } = this.props.location.query; // <== only natively available in react-router v3
fetch(`http://someAPI/recipe/id?recipeId=${recipeId}`)
.then(response => response.json())
.then(json => this.setState({ recipeDetail: json }));
}
render = () => (
!this.state.recipeDetails
? <div>Loading...</div>
: <ul>
{this.state.recipeDetail.map(ingredient => (
<li key={ingredient}>ingredient</li>
)}
</ul>
)
}
Another approach:
Have the recipeDetails stored and available within the original fetched recipes JSON. Then map over the recipes and create multiple <Card key={recipeId} recipeName={recipeName} recipeDetail={recipeDetail} /> components for each recipe.
which for simplicity sake might look like:
<div>
{this.state.recipes.map(({recipeId, recipeName, recipeDetail}), => (
<Card key={recipeId} recipeName={recipeName} recipeDetail={recipeDetail} />
)}
</div>
Then each individual Card has it's own state:
Card.js
export default class Card extends Component {
state = { showDetails: '' }
toggleShowDetails = () => this.setState(prevState => ({ showDetails: !this.state.showDetails }))
render = () => (
<div>
<h1>{this.props.recipeName} Recipe</h1>
<button onClick={toggleShowDetails}> {`${!this.state.showDetails ? "Show" : "Hide"} Recipe<button>
{ this.state.showDetails &&
<ul>
{this.props.recipeDetail.map(ingredient => (
<li key={ingredient}>ingredient</li>
)}
</ul>
}
)
}
Therefore, by default the recipeDetail is already there, but hidden. However, when a user clicks the Card's button, it will toggle the Card's showDetails state to true/false to display/hide the recipe detail.

Resources