Accessing state from another component in react - javascript

Just learning React and ran into a problem. I have a selector allowing me to choose year/make/model of a vehicle. However, I want it to reset make and model if the year is changed, or just reset the model if make is changed, so that those options state is set back to null and thus in the ui deselected.
However, the problem is I don't know how to make the state of a component available to the others. I figured the solution could be as simple as using an onChange function for year, that will then take the state of make/model and reset to null, though that's not possible without year.js knowing the state of the other 2...
Hopefully you understand what I'm talking about. Here's the code.
Year.js
import React, { Component } from 'react';
import '../CSS/App.css';
const vehicleYear = [
{id:1,label:"2019",href:"#"},
{id:2,label:"2018",href:"#"},
{id:3,label:"2017",href:"#"},
{id:4,label:"2016",href:"#"},
{id:5,label:"2015",href:"#"},
{id:6,label:"2014",href:"#"}
];
class Year extends Component {
constructor(props){
super(props);
this.state = {
year: null,
}
}
createYearList = () => {
let listItems = [];
for (let i = 0; i < vehicleYear.length; i++) {
listItems.push(
<li className={`list ${this.state.year === vehicleYear[i].id ? "active" : ""}`} onClick={(e) => {
this.yearClick(e, vehicleYear[i].id, vehicleYear[i].label)
}}>
<a href={vehicleYear[i].href}>{vehicleYear[i].label}</a>
</li>
);
}
return listItems;
};
yearClick = (e, id, label) => {
let state = this.state;
state.year = id;
this.setState(state);
console.log(this.state);
console.log(this.props.year);
};
render() {
return (
<div>
{this.createYearList()}
</div>
)
}
}
export default Year;
Make.js
import React, { Component } from 'react';
import '../CSS/App.css';
const vehicleMake = [
{id:1,label:"POLARIS",href:"#"},
{id:2,label:"CAN_AM",href:"#"},
{id:3,label:"YAMAHA",href:"#"},
{id:4,label:"SUZUKI",href:"#"},
{id:5,label:"ARCTIC-CAT",href:"#"}
];
class Make extends Component {
constructor(props){
super(props);
this.state = {
make: null
}
}
createMakeList = () => {
let listItems = [];
for(let i = 0; i < vehicleMake.length; i++){
listItems.push(
<li className={`list ${this.state.make === vehicleMake[i].id ? "active" : ""}`} onClick={(e)=>{this.makeClick(e, vehicleMake[i].id, vehicleMake[i].label)}}>
<a href={vehicleMake[i].href}>{vehicleMake[i].label}</a>
</li>
);
}
return listItems;
};
makeClick = (e, id, label) => {
console.log(id, label);
let state = this.state;
state.make = id;
this.setState(state);
console.log(state.make);
};
render() {
return (
<div>
{this.createMakeList()}
</div>
)
}
}
export default Make;
Model.js
import React, { Component } from 'react';
import '../CSS/App.css';
const vehicleModel = [
{id:1,label:"RZR 570",href:"#"},
{id:2,label:"RZR 900",href:"#"},
{id:3,label:"RZR S 900",href:"#"},
{id:4,label:"RZR S 1000",href:"#"},
{id:5,label:"RZR S 1000 TURBO",href:"#"}
];
class Model extends Component {
constructor(props){
super(props);
this.state = {
model: null
}
}
createModelList = () => {
let listItems = [];
for(let i = 0; i < vehicleModel.length; i++){
listItems.push(
<li className={`list ${this.state.model === vehicleModel[i].id ? "active" : ""}`} onClick={(e)=>{this.modelClick(e, vehicleModel[i].id, vehicleModel[i].label)}}>
<a href={vehicleModel[i].href}>{vehicleModel[i].label}</a>
</li>
);
}
return listItems;
};
modelClick = (e, id, label) => {
console.log(id, label);
let state = this.state;
state.model = id;
this.setState(state);
};
render() {
return (
<div>
{this.createModelList()}
</div>
)
}
}
export default Model;
And here's the main App.js
import React, { Component } from 'react';
import './CSS/App.css';
import { Container, Row, Col } from 'reactstrap';
import rzrPic from './Media/rzr-xp-1000-eps-trails-rocks-media-location-1-xxs.jpg';
import camsoT4S from './Media/camso-atv-t4s.jpg';
import Year from './Components/Year';
import Make from './Components/Make';
import Model from './Components/Model';
class App extends Component {
render() {
return (
<div className="App">
<Container fluid="true">
<Row>
<Col xs="3" className="categories">
<div>
<span className="categoryHeader">
<h2 className="categoryHeading">
VEHICLE YEAR
</h2>
</span>
<div className="categoryList">
<ul>
<Year/>
</ul>
</div>
</div>
<div>
<span className="categoryHeader">
<h2 className="categoryHeading">
VEHICLE MAKE
</h2>
</span>
<div className="categoryList">
<ul>
<Make/>
</ul>
</div>
</div>
<div>
<span className="categoryHeader">
<h2 className="categoryHeading">
VEHICLE MODEL
</h2>
</span>
<div className="categoryList">
<ul>
<Model/>
</ul>
</div>
</div>
</Col>
<Col xs="6" className="fill">
<img src={rzrPic} alt="rzr xp 1000"/>
</Col>
<Col xs="3" className="categories">
<span className="categoryHeader2">
<h2 className="categoryHeading">
AVAILABLE TRACKS
</h2>
</span>
<div className="Track">
<img src={camsoT4S} alt="Camso T4S Tracks"/>
<div className="TrackInfo">
<h3>CAMSO T4S - 4 SEASON</h3>
<p>Starting at $3,999.00</p>
<span>
ADD TO CART
</span>
</div>
</div>
<div className="Track">
<div className="TrackInfo">
<h3>CAMSO T4S - 4 SEASON</h3>
<p>Starting at $3,999.00</p>
<p className="select">SELECT</p>
</div>
</div>
</Col>
</Row>
</Container>
</div>
);
}
}
export default App;
Thanks in advance for your help!

Instead of storing the selected year / make / model in each component, store them in the parent App. You will then handle the reset logic in the App component.
Here is how to refactor your code:
import React, { Component } from "react";
import "../CSS/App.css";
// The list of years is now passed as props
//
// const vehicleYear = [];
class Year extends Component {
// You dont need the constructor anymore as the component
// doesn't have a state to initialize
//
// constructor(props) {}
createYearList = () => {
// Use the year list passed as a prop from the parent
const { vehicleYear } = this.props;
let listItems = [];
for (let i = 0; i < vehicleYear.length; i++) {
listItems.push(
<li
className={`list ${
this.state.year === vehicleYear[i].id ? "active" : ""
}`}
onClick={e => {
this.yearClick(e, vehicleYear[i].id, vehicleYear[i].label);
}}
>
<a href={vehicleYear[i].href}>{vehicleYear[i].label}</a>
</li>
);
}
return listItems;
};
yearClick = (e, id, label) => {
// Call the onClick function passed as a prop from the parent
this.props.onClick(e, id, label);
};
render() {
return <div>{this.createYearList()}</div>;
}
}
export default Year;
I only modified the Year component since the Make and Model components have the same structure. I'll come back to this later.
And here is how to use Year in App:
import React, { Component } from 'react';
// import ...
// Define the list of years
const vehicleYear = [
{id:1,label:"2019",href:"#"},
{id:2,label:"2018",href:"#"},
{id:3,label:"2017",href:"#"},
{id:4,label:"2016",href:"#"},
{id:5,label:"2015",href:"#"},
{id:6,label:"2014",href:"#"}
];
class App extends Component {
constructor(props){
super(props);
// Initialise the state of the App component
this.state = {
year: null,
}
}
// Move the click handler from the Year component to its parent component
yearClick = (e, id, label) => {
this.setState({
year: id
});
};
render() {
return (
<div className="App">
<Container fluid="true">
<Row>
<Col xs="3" className="categories">
<div>
<span className="categoryHeader">
<h2 className="categoryHeading">
VEHICLE YEAR
</h2>
</span>
<div className="categoryList">
<ul>
{/* Pass the list of years, the selected year and the handler to
select a year to the Year component */}
<Year vehicleYear={vehicleYear} selectedYear={this.state.year} onClick={this.yearClick} />
</ul>
</div>
</div>
...
</Row>
</Container>
</div>
);
}
}
export default App;
Now you have a fully controled Year and the logic handled by the App component. If you want to reset the selected year, you only have to create and call such a function in the App component:
resetYear = () => {
this.setState({
year: null
});
};
Bonus: Refactoring
You can refacto your Year, Make and Model components to one reusable component because they have exactly the same structure. Here is a ListComponent extracted from them:
// The list components takes three arguments:
// - itemsList: items to be displayed
// - selectedItemId: the id of the selected item
// - onSelect: a function to call when an item is selected
class ListComponent extends Component {
render() {
const { itemsList, selectedItemId, onSelect } = this.props;
return (
<div>
{itemsList.map(item => (
<li
className={`list ${selectedItemId === item.id ? "active" : ""}`}
onClick={e => {
onSelect(e, item.id, item.label);
}}
>
<a href={item.href}>{item.label}</a>
</li>
))}
</div>
);
}
}
export default ListComponent;
And you can use it like this:
<div>
<span className="categoryHeader">
<h2 className="categoryHeading">
VEHICLE YEAR
</h2>
</span>
<div className="categoryList">
<ul>
<ListComponent onSelect={this.selectYear} itemsList={vehicleYear} selectedItemId={this.state.year}/>
</ul>
</div>
</div>
<div>
<span className="categoryHeader">
<h2 className="categoryHeading">
VEHICLE MAKE
</h2>
</span>
<div className="categoryList">
<ul>
<ListComponent onSelect={this.selectMake} itemsList={vehicleMake} selectedItemId={this.state.make}/>
</ul>
</div>
</div>
<div>
<span className="categoryHeader">
<h2 className="categoryHeading">
VEHICLE MODEL
</h2>
</span>
<div className="categoryList">
<ul>
<ListComponent onSelect={this.selectModel} itemsList={vehicleModel} selectedItemId={this.state.model}/>
</ul>
</div>
</div>

The easiest solution will be to keep your state in the parent in this case in the App.js or create a component ad hoc to be the parent of your components, and simply pass a prop onChangeYear for example to change the state of your parent
import React, { Component } from 'react';
import './CSS/App.css';
import { Container, Row, Col } from 'reactstrap';
import rzrPic from './Media/rzr-xp-1000-eps-trails-rocks-media-location-1-xxs.jpg';
import camsoT4S from './Media/camso-atv-t4s.jpg';
import Year from './Components/Year';
import Make from './Components/Make';
import Model from './Components/Model';
class App extends Component {
contructor(props) {
super(props);
this.state = { year: "" }
}
render() {
return (
<div className="App">
<Container fluid="true">
<Row>
<Col xs="3" className="categories">
<div>
<span className="categoryHeader">
<h2 className="categoryHeading">
VEHICLE YEAR
</h2>
</span>
<div className="categoryList">
<ul>
<Year onChangeYear={(year) => {this.setState({year})/>
</ul>
</div>
</div>
<div>
<span className="categoryHeader">
<h2 className="categoryHeading">
VEHICLE MAKE
</h2>
</span>
<div className="categoryList">
<ul>
<Make/>
</ul>
</div>
</div>
<div>
<span className="categoryHeader">
<h2 className="categoryHeading">
VEHICLE MODEL
</h2>
</span>
<div className="categoryList">
<ul>
<Model/>
</ul>
</div>
</div>
</Col>
<Col xs="6" className="fill">
<img src={rzrPic} alt="rzr xp 1000"/>
</Col>
<Col xs="3" className="categories">
<span className="categoryHeader2">
<h2 className="categoryHeading">
AVAILABLE TRACKS
</h2>
</span>
<div className="Track">
<img src={camsoT4S} alt="Camso T4S Tracks"/>
<div className="TrackInfo">
<h3>CAMSO T4S - 4 SEASON</h3>
<p>Starting at $3,999.00</p>
<span>
ADD TO CART
</span>
</div>
</div>
<div className="Track">
<div className="TrackInfo">
<h3>CAMSO T4S - 4 SEASON</h3>
<p>Starting at $3,999.00</p>
<p className="select">SELECT</p>
</div>
</div>
</Col>
</Row>
</Container>
</div>
);
}
}
export default App;
If you find yourself having a lot of components in your web app I would think to integrate Redux to handle the state globally https://redux.js.org/introduction/getting-started.

Related

Console Error: Objects are not valid as a React child

I'm building a movie react app, basically Netflix, and I'm using bootstrap for most of my UI. I've managed to create a quick model of a login form that for now accepts any key and takes you to the movie list.
I've done my research but most of the question doesn't solve the issue or is unique to them.
Now here is where the problem starts: when I click on the movie card I get an error:
Objects are not valid as a React child (found: object with keys {Name,
Description}). If you meant to render a collection of children, use an
array instead.
Expected Output: Be able to load my movie once I click on the movie card
Question: How do I go about solving this issue?
Where I think the problem may be:
In the movie-view.jsx file
Source Code
movie-card.jsx
import React from 'react';
import propTypes from 'prop-types';
import Button from 'react-bootstrap/Button';
import Card from 'react-bootstrap/Card';
export class MovieCard extends React.Component {
render() {
const {
movieData,
onMovieClick
} = this.props;
return ( <
Card >
<
Card.Img variant = 'top'
className = 'thumbnail'
src = {
movieData.ImageURL
}
/> <
Card.Body >
<
Card.Title > {
movieData.Title
} < /Card.Title> <
Card.Text > {
movieData.Description
} < /Card.Text> <
Button onClick = {
() => onMovieClick(movieData)
}
variant = 'link' >
Open <
/Button> <
/Card.Body> <
/Card>
);
}
}
MovieCard.propTypes = {
movieData: propTypes.shape({
Title: propTypes.string.isRequired,
Description: propTypes.string.isRequired,
ImageURL: propTypes.string,
}).isRequired,
onMovieClick: propTypes.func.isRequired,
};
movie-view.jsx
import React from 'react';
export class MovieView extends React.Component {
keypressCallback(event) {
console.log(event.key);
}
componentDidMount() {
document.addEventListener('keypress', this.keypressCallback);
}
componentWillUnmount() {
document.removeEventListener('keypress', this.keypressCallback);
}
render() {
const { movieData, onBackClick } = this.props;
return (
<div className='movie-view'>
<div className='movie-poster'>
<img src={movieData.ImageURL} />
</div>
<div className='movie-title'>
<span className='label'>Title: </span>
<span>{movieData.Title}</span>
</div>
<div className='movie-description'>
<span className='label'> Description: </span>
<span className='value'> {movieData.Description}</span>
</div>
<div className='movie-director'>
<span className='label'>Director: </span>
<span>{movieData.Director}</span>
</div>
<div className='movie-genre'>
<span className='label'>Genre: </span>
<span className='value'>{movieData.Genre}</span>
</div>
<button
onClick={() => {
onBackClick(null);
}}
>
Back
</button>
</div>
);
}
}
Console
You're not accessing the movie data properly.
Your code:
<div className='movie-director'>
<span className='label'>Director: </span>
<span>{movieData.Director}</span>
</div>
<div className='movie-genre'>
<span className='label'>Genre: </span>
<span className='value'>{movieData.Genre}</span>
</div>
Solution:
<div className='movie-director'>
<span className='label'>Director: </span>
<span>{movieData.Director.Name}</span>
</div>
<div className='movie-genre'>
<span className='label'>Genre: </span>
<span className='value'>{movieData.Genre.Name}</span>
</div>
Here is a good article to read regarding this issue

pass some value back to parent component in next js

In my nextjs project I'm displaying posts from different tags and each post have many tags. I have a post_by_tags component and I'm using that component in different sections on home page to display posts from different tags. I don't want to show repeating content as some posts have same tags and I have a array to keep post ids that are visible to website. Now I want a way to keep post ids from child component to send back to parent component which updates the array so I can filter out these posts from post object. I find some examples but mostly these are tied with onclick or onchange something like that.
Here is my parent component code:
import Head from 'next/head'
import Image from 'next/image'
import Layout from '../components/Layout';
import Hero from '../components/Hero';
import Developed_country from '../components/Developed_country';
import Posts_by_tags from '../components/Post_by_tags';
import Attorneys from '../components/Attorneys';
import Business_formation from '../components/Business_formation';
import Case from '../components/Case';
export async function getServerSideProps(context) {
// Fetch data from external API
const res = await fetch(`https://dashboard.toppstation.com/api/blogs`);
const data = await res.json();
// Pass data to the page via props
return {
props: { blogs:data}
}
}
export default function Home({blogs}) {
const blog_post_id = [];
const pull_data = (data) => {
console.log(data); // LOGS DATA FROM CHILD)
}
return (
<Layout>
<Hero/>
<Developed_country/>
<Posts_by_tags tag='business' bg='bg_grey' posts={blogs} func={pull_data}/>
<Business_formation/>
<Attorneys/>
<Posts_by_tags tag='png' bg='bg_white' posts={blogs} />
<Posts_by_tags tag='image' bg='bg_grey' posts={blogs} />
<Posts_by_tags tag='png' bg='bg_white' posts={blogs} />
<Case/>
</Layout>
)
}
Child component:
import Blogimg from '../public/img/blog.png';
import btn_arrow from '../public/img/btn_arrow.svg';
import styles from '../styles/Home.module.css';
export default function Posts_by_tags(props){
props.func('My name is xyz');
const bg = props.bg;
let align = ['start','center','end'];
let post_tags = [];
const postIds= [];
const blog_posts = props.posts.filter(bpost=>{
bpost.tags.forEach(tag => {
if(tag.toLowerCase() === props.tag){
return postIds.push(bpost._id);
}
})
});
const posts = props.posts.filter(p => {
if( (postIds.indexOf(p._id) !== -1) && p.visibility==true){
return p;
}
}).slice(0,3);
return(
<>
{posts.length == 0 ? null :(
<section id={styles.postsbytags} className={bg}>
<div className='wrapper'>
<div className="container section posts_by_tags section_ptb">
<div className='row'>
<div className='col-sm-12'>
<h3 className={`${styles.heading3} text-center`}><span className={`${styles.heading3span} ${bg}`}>{props.tag}</span></h3>
</div>
</div>
<div className='row pt_100'>
{posts.map( (post, index) =>(
<div id={`post-${post._id}`} className={`col-md-4 d-flex justify-content-md-${align[index]} justify-content-center`} key={post._id}>
<div className={styles.blog_post}>
<div className={`${styles.blog_image} text-center`}>
<span className={styles.blog_tag}>{props.tag}</span>
<Image className="img-fluid" src={post.image} alt={post.title} width={450} height={400} layout='responsive'/>
</div>
<div className='blog_content'>
<h4 className={styles.blog_title}>{post.title}</h4>
<p className={styles.blog_desc}>{post.description.split(' ').slice(0, 10).join(' ')}...</p>
</div>
</div>
</div>
))}
</div>
<div className='row'>
<div className='col-sm-12'>
<div className='blog_category pt_50'>
<a href="" className={ `btn ${styles.btn_tags} `}>See More {props.tag} <i className={styles.btn_icon}><Image src={btn_arrow} alt="btn-icon"/></i></a>
</div>
</div>
</div>
</div>
</div>
</section>
)}
</>
);
}```

ReactJs Hiding Some HTML depend on situation

I want to make the price tag also some other HTML contents hide/show depending on some data entry.
for example, if I get True it should be visible prices if it's gonna be False it must hide.
I'm sharing some code of my pages please give me ideas.
Thank you.
// react
import React from 'react';
// third-party
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
// application
import AsyncAction from './AsyncAction';
import Points from './Points';
import { cartAddItem } from '../../store/cart';
import { Quickview16Svg } from '../../svg';
import { quickviewOpen } from '../../store/quickview';
import { url } from '../../services/utils';
function ProductCard(props) {
const {
product,
layout,
quickviewOpen,
cartAddItem,
} = props;
const containerClasses = classNames('product-card', {
'product-card--layout--grid product-card--size--sm': layout === 'grid-sm',
'product-card--layout--grid product-card--size--nl': layout === 'grid-nl',
'product-card--layout--grid product-card--size--lg': layout === 'grid-lg',
'product-card--layout--list': layout === 'list',
'product-card--layout--horizontal': layout === 'horizontal',
});
let badges = [];
let image;
let price;
let features;
if (product.badges.includes('sale')) {
badges.push(<div key="sale" className="product-card__badge product-card__badge--sale">Sale</div>);
}
if (product.badges.includes('hot')) {
badges.push(<div key="hot" className="product-card__badge product-card__badge--hot">Hot</div>);
}
if (product.badges.includes('new')) {
badges.push(<div key="new" className="product-card__badge product-card__badge--new">New</div>);
}
badges = badges.length ? <div className="product-card__badges-list">{badges}</div> : null;
if (product.images && product.images.length > 0) {
image = (
<div className="product-card__image product-image">
<Link to={url.product(product)} className="product-image__body">
<img className="product-image__img" src={product.images[0]} alt="" />
</Link>
</div>
);
}
if (product.discountPrice) {
price = (
<div className="product-card__prices">
<span className="product-card__new-price"><Points value={product.price} /></span>
{' '}
<span className="product-card__old-price"><Points value={product.discountPrice} /></span>
</div>
);
} else {
price = (
<div className="product-card__prices">
<Points value={product.price} />
</div>
);
}
if (product.attributes && product.attributes.length) {
features = (
<ul className="product-card__features-list">
{product.attributes.filter((x) => x.featured).map((attribute, index) => (
<li key={index}>{`${attribute.name}: ${attribute.values.map((x) => x.name).join(', ')}`}</li>
))}
</ul>
);
}
return (
<div className={containerClasses}>
<AsyncAction
action={() => quickviewOpen(product.slug)}
render={({ run, loading }) => (
<button
type="button"
onClick={run}
className={classNames('product-card__quickview', {
'product-card__quickview--preload': loading,
})}
>
<Quickview16Svg />
</button>
)}
/>
{badges}
{image}
<div className="product-card__info">
<div className="product-card__name">
<Link to={url.product(product)}>{product.name}</Link>
<br />
<br />
</div>
{features}
</div>
<div className="product-card__actions">
<div className="product-card__availability">
Availability:
<span className="text-success">In Stock</span>
</div>
{price}
<div className="product-card__buttons">
<AsyncAction
action={() => cartAddItem(product)}
render={({ run, loading }) => (
<React.Fragment>
<button
type="button"
onClick={run}
className={classNames('btn btn-primary product-card__addtocart', {
'btn-loading': loading,
})}
>
Add To Cart
</button>
<button
type="button"
onClick={run}
className={classNames('btn btn-secondary product-card__addtocart product-card__addtocart--list', {
'btn-loading': loading,
})}
>
Add To Cart
</button>
</React.Fragment>
)}
/>
</div>
</div>
</div>
);
}
ProductCard.propTypes = {
/**
* product object
*/
product: PropTypes.object.isRequired,
/**
* product card layout
* one of ['grid-sm', 'grid-nl', 'grid-lg', 'list', 'horizontal']
*/
layout: PropTypes.oneOf(['grid-sm', 'grid-nl', 'grid-lg', 'list', 'horizontal']),
};
const mapStateToProps = () => ({});
const mapDispatchToProps = {
cartAddItem,
quickviewOpen,
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(ProductCard);
Here I want to hide prices in some onload situations. This is my homepage Carousel.
You can use code like this. It will only render the component if the boolean evaluates to a truthy value.
const { isVisible } = this.props; // Or wherever you want to get your boolean from
return (
<div>
{isVisible && <MyComponent />}
</div>
You refer to Conditional rendering, there are a couple ways to do that:
<div>
{someCondition && <p>The condition is true</p>}
</div>
Or if you want a if else rendering:
<div>
{someCondition ? <p>The condition is true</p> : <p>The condition is false</p>}
</div>
You can find more info in react docs

React, how to access child's state from parent? no need to update parent's state

Hi I am pretty new to React and having really hard time wrapping my head around this whole state management and passing data through state and props. I do understand that the standard react way is to pass down data in a unidirectional way- from parent to child, which I have done so for all other components.
But I have this component called Book, which changes its 'shelf' state, based on user selection form 'read, wantToRead, currentlyReading, and none'. And in my BookList component which renders Book component, but it needs to be able to read Book's shelf state and render the correct books under sections called 'read, wantToRead, currentlyReading, and none'. And since in this case, Book component is being rendered from BookList component and BookList being the parent, i really cannot understand how to enable BookList to access Book's state?
BookList component:
import React, { Component } from 'react'
import { Link } from 'react-router-dom'
import Book from './Book'
class BookList extends Component {
render(){
const { books, shelf } = this.props
return (
<div className="list-books">
<div className="list-books-content">
<div className="list-books-title">
<h1>MyReads</h1>
</div>
<div className="bookshelf">
<h2 className="bookshelf-title">None</h2>
{books.map((book)=> {
console.log(book)
if (shelf === ''){
return <div className="bookshelf-books">
{/* <BookStateless book= {book} /> */}
<Book book = {book} />
{/* <BookStateless book= {book} /> */}
</div>
}
})}
</div>
<div className="bookshelf">
<h2 className="bookshelf-title">Currently Reading</h2>
{books.map((book)=> {
if (shelf === 'currentlyReading'){
return <div className="bookshelf-books">
{/* <BookStateless book= {book} /> */}
<Book book = {book} />
</div>
}
// console.log(this.book.title, this.book.state)
})}
</div>
<div className="bookshelf">
<h2 className="bookshelf-title">Want to Read</h2>
{books.map((book)=> {
if (shelf === 'wantToRead'){
return <div className="bookshelf-books">
{/* <BookStateless book= {book} /> */}
<Book book = {book} />
{/* <BookStateless book= {book} /> */}
</div>
}
// console.log(this.book.title, this.book.state)
})}
</div>
<div className="bookshelf">
<h2 className="bookshelf-title">Read</h2>
{books.map((book)=> {
if (shelf === 'read'){
console.log(shelf)
return <div className="bookshelf-books">
{/* <BookStateless book= {book} /> */}
<Book book = {book} />
</div>
}
// console.log(this.book.title, this.book.state)
})}
</div>
</div>
<div className="open-search">
<Link to="/search">Add a book</Link>
</div>
</div>
)
}
}
export default BookList
Book component:
import React, { Component } from 'react'
// import * as BooksAPI from './BooksAPI'
import Select from 'react-select'
import 'react-select/dist/react-select.css'
class Book extends Component {
state={
// state can be read, none, want to read, or currently reading
shelf: ''
}
handleChange(e){
this.setState({ shelf: e['value'] })
console.log("this?", this)
}
render(){
const { book } = this.props
const { shelf } = this.state
console.log("book", book.state)
const options = [
{ value: 'currentlyReading', label: 'currentlyReading'},
{ value: 'wantToRead', label: 'wantToRead'},
{ value: 'read', label: 'read'},
{ value: 'none', label: 'none'}
]
return (
<li key={book.id}>
<div className="book">
<div className="book-top">
<div className="book-cover" style={{ width: 128, height: 188, backgroundImage: `url("${book.imageLinks.thumbnail}")` }}></div>
<div className="book-shelf-changer">
<Select
value=""
options={options}
onChange={(e)=>this.handleChange(e)}
/>
</div>
</div>
<div className="book-title">{book.title}</div>
<div className="book-authors">{book.authors}</div>
</div>
</li>
)
}
}
export default Book
in my app.js i have:
import React from 'react'
import * as BooksAPI from './BooksAPI'
import './App.css'
import Search from './Search'
import BookList from './BookList'
import Book from './Book'
import { Route } from 'react-router-dom'
class BooksApp extends React.Component {
state = {
books : []
}
componentDidMount(){
BooksAPI.getAll().then((books)=> {
this.setState({ books: books })
// console.log("bookstest",this)
})
}
render() {
return (
<div className="App">
<Route exact path="/" render={()=>(
<Book books={this.state.books} />
)} />
<Route path="/search" render={()=>(
<Search books={this.state.books} />
)} />
<Route path="/BookList" render={()=>(
<BookList books={this.state.books} />
)} />
</div>
)
}
}
export default BooksApp
Right now, when i open the booklist component in browser, i get no books because it's not picking up the state in any of the if statements here:
if (shelf === 'currentlyReading'){
return <div className="bookshelf-books">
}
Thank you so much in advance for reading through and, any help would be much appreciated!
Thank you!
You don't need to "access" the child's state, you can pass a callback handler from the parent to the child and when an event is triggered inside the child you can notify the parent through that event handler (callback).
I'll post a small example:
class Book extends React.Component {
handleClick = e => {
const { bookId, onToggleBook } = this.props;
onToggleBook(bookId);
};
render() {
const { name, isRead } = this.props;
return (
<div className={`${isRead && "read"}`} onClick={this.handleClick}>
<span>{name}</span>
{isRead && <i> - You read me</i> }
</div>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
books: [
{
id: 1,
name: "book 1",
isRead: false
},
{
id: 2,
name: "book 2",
isRead: false
},
{
id: 3,
name: "book 3",
isRead: true
},
{
id: 4,
name: "book 4",
isRead: false
}
]
};
}
onToggleBookStatus = bookid => {
const { books } = this.state;
const nextBookState = books.map(book => {
if (book.id !== bookid) return book;
return {
...book,
isRead: !book.isRead
};
});
this.setState(prevState => ({ books: nextBookState }));
};
render() {
const { books } = this.state;
return (
<div>
<div>My Books</div>
{books.map(book => (
<Book
key={book.id}
isRead={book.isRead}
name={book.name}
bookId={book.id}
onToggleBook={this.onToggleBookStatus}
/>
))}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
.read {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
As you know, to pass something from the parent to the child, you use props. To get something from the child to the parent, you again use props, but this time you pass a function down to the child, and then the child calls that function.
So for example, you would modify the child's handle change function to something like this:
handleChange(e){
if (this.props.onShelfChanged) {
this.props.onShelfChanged(e.value);
}
this.setState({ shelf: e.value })
}
And then in the parent, you'll want to pass an onShelfChanged prop down to the book, so that you can get notified when the value changes. Something like this:
// in the render function
{books.map((book, index) =>
<Book book={book} onShelfChanged={() => this.childBookChanged(index)}
)};
And you'll need to create and fill out the childBookChanged function to do whatever updates you need to do.
One thing to be mindful of is that you don't want to be manually keeping the book and the bookshelf in sync. The Book is tracking some state of its own, and then you're passing that up and probably altering the state of the bookshelf. Keeping these in sync as your application grows can be a headache and a source of bugs. Instead, you should have one piece of code be in charge, which it looks like will likely be the bookshelf (since it's the topmost component that cares about this state). So most likely you'll want to remove the internal state from Book, and instead tell the book what to do via props.
If you need the Book component to sometimes work as a standalone and sometimes work inside a bookshelf, then you may need to do a bit more work to get it to support both a "controlled" and "uncontrolled" implementation, but it's still a good idea to move the state up for the controlled case. You can read more about controlled and uncontrolled components here and here

Passing data to parent - recipebook React

I am working on the recipe book app and trying to implement the edit entry function.
How it works is to input recipe name (e.g Pasta), followed by ingredients (e.g egg, flour, salt). The ingredients have to be input with commas and will be shown as a list.
Pasta
-Egg
-Flour
i can see that it is somewhat working, because i can see the new entries in the input text (e.g initially was egg,flour,salt -> egg,flour,salt,water) when i tried to edit it again.
However, the extra ingredients (in the above example: water) is not showing up in the list. Do i have to figure a way to re-render the list?
updates:
I think i know where the error might be. There is some issue passing the data and setting the state.
<EditRecipe recipe={this.props.recipe} editRecipe={this.editRecipe.bind(this, this.props.recipe.id, recipe)}/>
App.js
import React, { Component } from 'react';
// import logo from './logo.svg';
import './App.css';
import uuid from 'uuid';
import Modal from 'react-modal';
import RecipeList from './components/RecipeList/RecipeList';
import AddRecipe from './components/AddRecipe/AddRecipe';
class App extends Component {
constructor(props){
super(props);
this.state = {
recipes:[]
};
}
getRecipes(){
this.setState({recipes:[
{
id: uuid.v4(),
food: "pumpkin pie",
ingredients: ["pumpkin puree", "sweetened condensed milk", "eggs", "pumpkin pie spice", "pie crust"]
},
{
id: uuid.v4(),
food: "spaghetti",
ingredients: ["noodles", "tomato sauce", "meatballs"]
},
{
id: uuid.v4(),
food: "onion pie",
ingredients: ["onion", "pie crust"]
},
]});
}
componentWillMount(){
this.getRecipes();
}
handleAddRecipe(recipe){
let recipes = this.state.recipes;
recipes.push(recipe);
this.setState({recipes: recipes});
}
handleDeleteRecipe(id){
let recipes = this.state.recipes;
let index = recipes.findIndex(x => x.id === id);
recipes.splice(index,1);
this.setState({recipes: recipes});
}
handleEditRecipe(id, recipe){
let recipes = this.state.recipes;
let index = recipes.findIndex(x => x.id === id);
recipes.splice(index,1,recipe);
this.setState({recipes: recipes});
}
render() {
return (
<div className="App">
<RecipeList recipes={this.state.recipes} onDelete={this.handleDeleteRecipe.bind(this)} onEdit={this.handleEditRecipe.bind(this)}/>
<AddRecipe addRecipe={this.handleAddRecipe.bind(this)}/>
</div>
);
}
}
export default App;
RecipeList.js
import React, { Component } from 'react';
import Collapsible from 'react-collapsible';
import RecipeItem from '../RecipeItem/RecipeItem'
import './RecipeList.css';
class RecipeList extends Component{
deleteRecipe(id){
this.props.onDelete(id);
}
editRecipe(id, recipe){
this.props.onEdit(id, recipe);
}
render(){
let recipeItem;
if(this.props.recipes){
recipeItem=this.props.recipes.map(recipe => {
return(
<RecipeItem onEdit={this.editRecipe.bind(this)} onDelete={this.deleteRecipe.bind(this)} key={recipe.id} recipe={recipe} />
)
});
}
return(
<div className="recipeList box">
{recipeItem}
</div>
)
}
}
export default RecipeList;
RecipeItem.js
import React, { Component } from 'react';
import Collapsible from 'react-collapsible';
import EditRecipe from '../EditRecipe/EditRecipe';
class RecipeItem extends Component{
deleteRecipe(id){
this.props.onDelete(id);
}
editRecipe(id, recipe){
this.props.onEdit(id, recipe);
}
render(){
let recipe=this.props.recipe
let foodName=recipe.food;
let ingredientItem;
if(recipe.ingredients){
ingredientItem=recipe.ingredients.map(ingredient=>{
return(
<a className="panel-block">
{ingredient}
</a>
)
})
}
return(
<ul>
<li className="Recipe">
<Collapsible trigger={foodName} transitionTime="200" easing="ease-in-out">
<nav className="panel">
<p className="panel-heading">
Ingredients
</p>
{ingredientItem}
<div className="panel-block">
<button className="button is-warning is-outlined" onClick={this.deleteRecipe.bind(this, this.props.recipe.id)}>
Delete
</button>
<EditRecipe recipe={this.props.recipe} editRecipe={this.editRecipe.bind(this, this.props.recipe.id, recipe)}/>
</div>
</nav>
</Collapsible>
</li>
</ul>
);
}
}
export default RecipeItem;
EditRecipe.js
import React, { Component } from 'react';
import RecipeForm from '../RecipeForm/RecipeForm';
// import './EditRecipe.css';
import Modal from 'react-modal';
import uuid from 'uuid';
// import Modal from 'boron/DropModal';
// import './RecipeList.css';
class RecipeEdit extends Component{
constructor(props){
super(props);
this.state = {
revisedRecipe:{
id: this.props.recipe.id,
food: this.props.recipe.food,
ingredients: this.props.recipe.ingredients
},
modalIsOpen: false,
speed: 100
};
this.openModal = this.openModal.bind(this);
this.closeModal = this.closeModal.bind(this);
}
openModal(){
this.setState({modalIsOpen: true});
}
closeModal(){
this.setState({modalIsOpen: false});
}
handleSubmit(e){
const revised = this.state.revisedRecipe;
this.props.editRecipe(revised);
e.preventDefault();
}
handleNameChange(e){
this.setState({revisedRecipe:{
food: e.target.value
}
});
}
handleIndChange(e){
this.setState({revisedRecipe:{
ingredients: e.target.value
}
});
}
render(){
const speed = this.state.speed;
let recipe=this.props.recipe;
let foodName=this.state.revisedRecipe.food;
let ingredients=recipe.ingredients;
return(
<div>
<button className="button is-primary" onClick={this.openModal}>Edit Recipe</button>
<Modal
isOpen={this.state.modalIsOpen}
onAfterOpen={this.afterOpenModal}
onRequestClose={this.closeModal}
closeTimeoutMS={speed}
contentLabel="Example Modal"
>
<div className="field">
<h2 className="title is-2">Edit Recipe</h2>
<form>
<label className="label">Recipe</label>
<div className="control">
<input className="input" type="text" placeholder="Recipe Name" ref="recipeName" value={this.state.revisedRecipe.food} onChange={this.handleNameChange.bind(this)}/>
</div>
<div className="field">
<label className="label">Ingredients</label>
<div className="control has-icons-left has-icons-right">
<input className="input" type="text" placeholder="Enter ingredients. (if more than 1 ingredient, separate them with commas)" ref="ingredients" value={this.state.revisedRecipe.ingredients} onChange={this.handleIndChange.bind(this)}/>
<span className="icon is-small is-left">
<i className="fa fa-flask"></i>
</span>
</div>
</div>
<div className="field is-grouped">
<div className="control">
<button className="button is-primary" onClick={this.closeModal}>Edit Recipe</button>
</div>
<div className="control">
<button className="button" onClick={this.closeModal}>Cancel</button>
</div>
</div>
</form>
</div>
</Modal>
</div>
);
}
}
export default RecipeEdit;
I believe you're actually getting an error when trying to re-render after updating a list. The ingredients property in the recipes are an array (as shown in getRecipes()) but you're setting the new state of ingredients (in EditRecipe) as a string: "egg,flour,salt,water" and then trying to render the ingredients as an array: ingredients.map().
When you render an input field with an array <input value={["egg", "flour"]} /> it does show the values separated by comma, but the event.target.value in onChange is actually a string.
In EditRecipe's, handleIndChange could be fixed with:
this.setState({revisedRecipe: {ingredients: e.target.value.split(',')}});
This does have another problem, though in that you are overriding the revisedRecipe completely. So all of the setState calls should be something like:
const recipe = this.state.revisedRecipe;
recipe.ingredients = e.target.value.split(',');
this.setState({revisedRecipe: recipe);

Categories

Resources