How to refactor react component separate for rendering and logic? - javascript

I have react component:
function FavoritesListItem({ merchant, config, isFavorited }) {
const {
name, id, logoUrls = {}, offersCount, rebate, showRebate,
} = merchant;
const { rebateOptions } = config;
return (
const renderActiveMerchant = () => (
<div
className="mn_favoriteMerchant"
data-merchant-id={id}
data-merchant-name={name}
role="listitem"
data-test="favorite-merchant"
>
<div className="mn_favoriteMerchantInner">
<MerchantExperienceLink
className="mn_favoriteMerchantLink"
merchant={merchant}
title={`Opens merchant detail page at ${name}`}
>
<FavoriteIcon
merchantId={id}
merchantName={name}
labelUnfavorite={`Remove ${name} from Favorites list`}
showSpinner={!isFavorited}
/>
<div className="mn_logo"><img data-test="favorited-merchant-logo" src={logoUrls._120x60} alt={name} /></div>
<p className="mn_offersCount" data-test="favorited-merchant-offers-count">{offersCount} offers available </p>
</MerchantExperienceLink>
{rebate && (
<MerchantClickUrlLink className="mn_favoriteMerchantRebateLink" merchant={merchant}>
<div className="mn_rebate">
{showRebate
? <MerchantRebate {...rebate} {...rebateOptions} />
: <MerchantNoRebateLabel />}
? <MerchantRebate {...rebate} {...rebateOptions} />
: <MerchantNoRebateLabel />}
</div>
</MerchantClickUrlLink>
)}
</div>
</div>
);
const renderDeactivatedMerchant = () => (
<div
className="mn_favoriteMerchant"
data-merchant-id={id}
data-merchant-name={name}
role="listitem"
data-test="favorite-merchant"
>
<div className="mn_favoriteMerchantInner">
<MerchantExperienceLink
className="mn_favoriteMerchantLink"
merchant={merchant}
title={`Opens merchant detail page at ${name}`}
>
<FavoriteIcon
merchantId={id}
merchantName={name}
labelUnfavorite={`Remove ${name} from Favorites list`}
showSpinner={!isFavorited}
/>
<div className="mn_logo mn_noRebateMerchantLogo">
<img data-test="favorited-merchant-logo" src={logoUrls._120x60} alt={name} />
</div>
{rebate && (
<div className="mn_rebate mn_deactivatedRebate">
{
showRebate
? <MerchantNoRebateLabel />
: <MerchantRebate {...rebate} />
}
</div>
)}
</MerchantExperienceLink>
</div>
</div>
);
return (
merchant.type === 'Deactivated Merchant' ? renderDeactivatedMerchant() : renderActiveMerchant()
);
}
const mapStateToProps = () => {
const selectFavoriteByMerchantId = makeSelectFavoriteByMerchantId();
return (state, { merchant }) => ({
isFavorited: selectFavoriteByMerchantId(state, merchant.id),
});
};
export default connect(mapStateToProps)(FavoritesListItem);
and need to refactor it to 2 separate components which will be just render renderDeactivatedMerchant and renderActiveMerchant. All other logic should be in this component FavoritesListItem
So I created components this way:
export class FavoritesListItemDeactivatedMerchant extends Component ({ merchant, config, isFavorited }) {
render() {
const { merchant, config, isFavorited } = this.props;
const {
name, id, logoUrls = {}, rebate, showRebate,
} = merchant;
const { rebateOptions } = config;
return (
<div
className="mn_favoriteMerchant"
data-merchant-id={id}
data-merchant-name={name}
role="listitem"
data-test="favorite-merchant"
>
<div className="mn_favoriteMerchantInner">
<MerchantExperienceLink
className="mn_favoriteMerchantLink"
merchant={merchant}
title={`Opens merchant detail page at ${name}`}
>
<FavoriteIcon
merchantId={id}
merchantName={name}
labelUnfavorite={`Remove ${name} from Favorites list`}
showSpinner={!isFavorited}
/>
<div className="mn_logo mn_noRebateMerchantLogo">
<img data-test="favorited-merchant-logo" src={logoUrls._120x60} alt={name} />
</div>
{rebate && (
<div className="mn_rebate mn_deactivatedRebate">
{
showRebate
? <MerchantNoRebateLabel />
: <MerchantRebate {...rebate} />
}
</div>
)}
</MerchantExperienceLink>
</div>
</div>
);
}
}
const mapStateToProps = () => {
const selectFavoriteByMerchantId = makeSelectFavoriteByMerchantId();
return (state, { merchant }) => ({
isFavorited: selectFavoriteByMerchantId(state, merchant.id),
});
};
export default connect(mapStateToProps)(FavoritesListItemDeactivatedMerchant);
Project builded without errors and also no errors in console. But it's not render this component in browser. What I'm doing wrong? Please, help.

You have confused the syntax for function and class components and created a weird mash-up that combines both:
export class FavoritesListItemDeactivatedMerchant extends Component ({ merchant, config, isFavorited }) {
Those props make no sense in a class! I think you meant to write this:
export const FavoritesListItemDeactivatedMerchant = ({ merchant, config, isFavorited }) => {
or
export function FavoritesListItemDeactivatedMerchant({ merchant, config, isFavorited }) {
Personally I think you can improve this code by having a shared RenderMerchant with a prop isDeactivated instead of having separate components for active and deactivated. There is a lot of repeated code between the two cases which you want to avoid.

Related

Passing states between components in React

I am wondering the best way to tackle this problem.
I intend to create a state in the parent component of my application, pass the set state to a component that holds the button (sibling A), and the value to another component where it will be displayed and updated (sibling B). I have a rough idea of how to do this, but ultimately I am lost and am seeking direction. Thank you!
My parent component is as follows
//playlist state
const [playlistItem, setPlaylistItem] = useState()
const mainCards = Data.map(card => {
return(
<MainCard
key={card.id}
id={card.id}
image={card.url}
title={card.title}
playbutton={card.playbutton}
addbutton={card.addbutton}
playlistState={setPlaylistItem}
/>
)
})
const sideCards = SideData.map(card => {
return(
<SideCard
image={card.sideurl}
key={card.id}
title={card.sidetitle}
playbutton={card.playbutton}
addbutton={card.addbutton}
playlistItem={playlistItem}
/>
)
})
return (
<div className="App">
{console.log("main cards" + mainCards[0])}
{console.log("side cards" + sideCards.sidetitle)}
<Navbar />
<Header />
<Playlist />
<CardContainer />
<div className="maincards">
{mainCards}
</div>
<div className="sidecards">
{sideCards}
</div>
</div>
)
}
Sibling A
const handleAdd = (id) => {
console.log(id)
}
return(
<div>
<div className="mainCardObject">
<div className="cardObj">
<img src={props.image} className ="mainCardImage"/>
<img src={props.playbutton} className="playbutton"/>
<img src={props.addbutton} onClick={() => handleAdd(props.id)} className="addbutton" />
</div>
</div>
</div>
)
}
Sibling B
function Playlist(props){
return(
<div className="playlistContainer">
<ul>
<li></li>
</ul>
</div>
)
}
You can pass both getter and setter as parameters, and use them normally in your components:
Sibling A
const handleAdd = (id) => {
//your logic
props.playlistState(prevValue => [])
}
Sibling B
return (
<div className="playlistContainer">
<ul>
<li>
{props.playlistItem}
</li>
</ul>
</div>
)
You can also create a function in your parent component, using set state, and pass this function as a parameter to your component:
Parent
const addPlayListItem = (playListItem) => {
setPlaylistItem(prev => [...prev, playListItem])
}
return (
<SiblingA
addPlayListItem={addPlayListItem}
/>
)
Sibling A
function MainCard(props) {
const handleAdd = () => {
//your logic
props.addPlayListItem({ name: 'rock', title: 'Foo Fighters', year: 2011 })
}
return (
<div>
<img src={props.addbutton} onClick={handleAdd} className="addbutton" />
</div>
)
}
Sibling A:
const handleAdd = (id) => {
props.playlistState((prev) => ([...prev, id]))
}

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

How to identify a single div element among set of elements in react?

I have a react app with a list of Div elements to create some Cards. Each card has 'read more' button to expand and collapse a paragraph and I toggle it for each mouse click. My problem is, for each click, it expand paragraphs in all cards instead only paragraph in the card I clicked. So I can't identify the clicked (this) card.
Component:
class BidCard extends Component {
constructor(props) {
super(props);
this.state = {
readMoreOpen: false,
}
}
readMore() {
this.setState({ readMoreOpen: !this.state.readMoreOpen })
}
render() {
const { articles } = this.props;
return (
articles.map(article => {
return (
<div className="projectCardRoot" key={article.id}>
<div className="projectCardMainLogin">
<div className="projectCardMiddle">
<p className={this.state.readMoreOpen ? 'openFullParagraph' : 'closeFullParagraph'} id="projectCardDesc">{article.description}</p>
<div className="cardReadMore desktopDiv" onClick={this.readMore.bind(this)}>Read more</div>
</div>
</div>
</div>
)
})
)
}
}
export default BidCard;
How can I solve this?
You can save id of the expanded card to the state and the check it when rendering items:
class BidCard extends Component {
constructor(props) {
super(props);
this.state = {
readMoreOpen: [], // Use array here
}
}
// Add card id to the expanded array if not already there, otherwise remove it
readMore = (id) => {
this.setState(state => {
if (state.readMoreOpen.includes(id)) {
return {readMoreOpen: state.readMoreOpen.filter(readId => readId !== id)}
}
return {readMoreOpen: [...state.readMoreOpen, id]}
})
}
render() {
const { articles } = this.props;
return (
articles.map(article => {
return (
<div className="projectCardRoot" key={article.id}>
<div className="projectCardMainLogin">
<div className="projectCardMiddle">
{/*Check if the item is in expanded items array */}
<p className={this.state.readMoreOpen.includes(article.id) ? 'openFullParagraph' : 'closeFullParagraph'} id="projectCardDesc">{article.description}</p>
<div className="cardReadMore desktopDiv" onClick={() => this.readMore(article.id)}>Read more</div>
</div>
</div>
</div>
)
})
)
}
}
You will need to keep expanded state per every card.
I would recommend to create component for card
articles.map(article => {
return (
<Article key={article.id} {...article} />
)
})
)
class Article extends Component {
state = {
readMoreOpen: false
}
readMore() {
this.setState(state => ({ readMoreOpen: !state.readMoreOpen }))
}
render () {
const {description} = this.props;
return (<div className="projectCardRoot" >
<div className="projectCardMainLogin">
<div className="projectCardMiddle">
<p className={this.state.readMoreOpen ? 'openFullParagraph' : 'closeFullParagraph'} id="projectCardDesc">{description}</p>
<div className="cardReadMore desktopDiv" onClick={this.readMore.bind(this)}>Read more</div>
</div>
</div>
</div>)
}
}
Other approach is to keep array of booleans with information of which article div should be currently expanded in this method you will need to update state with id of expanded article
readMore(id) {
this.setState({ articles: this.props.articles.map(article => article.id === id ? true : false) } )
}
and in render use boolean from state as information if it should be expanded
That's because all your cards currently share the same source of truth. You used a ternary operator to determine what class a Card would have depending on the state-value. Well, all Cards are using the same state-value to compare, so understandably, if one is affected, then all would be too.
There's more than one way to resolve this, but the most appropriate would probably be to create a separate Card Component. This makes it so each Card component has their own state to keep track of.
See working sandbox: https://codesandbox.io/s/quizzical-mahavira-wz8iu
Parent.js
import React from "react";
import ReactDOM from "react-dom";
import Card from "./Card";
import "./styles.css";
class BidCard extends React.Component {
render() {
const { articles } = this.props;
return articles.map(article => {
return <Card article={article} />;
});
}
}
BidCard.defaultProps = {
articles: [{ description: "woof" }, { description: "meow" }]
};
const rootElement = document.getElementById("root");
ReactDOM.render(<BidCard />, rootElement);
Card.js
import React, { useState } from "react";
const Card = ({ article }) => {
const [readOpen, setReadOpen] = useState(false);
return (
<div className="projectCardRoot" key={article.id}>
<div className="projectCardMainLogin">
<div className="projectCardMiddle">
<p
className={readOpen ? "openFullParagraph" : "closeFullParagraph"}
id="projectCardDesc"
>
{article.description}
</p>
<div
className="cardReadMore desktopDiv"
onClick={() => setReadOpen(!readOpen)}
>
Read more
</div>
</div>
</div>
</div>
);
};
export default Card;
I did a few modifications to your code. This way it should work.
I added comments that explain the the changes. The main idea is that you should not simply store the boolean readMoreOpen status (which in your code is treated as a kind of shared between all the cards) but specific card identity.
My changes works if there could be only one "expanded" card at any moment. If your design supposes that there could be a few "expanded" cards at the same time the solution would be more complex though not much.
class BidCard extends Component {
constructor(props) {
super(props);
// the way you've tried to keep status (open/closed) it wasn't tied to any speciifc card
// you should store this specific card instead
this.state = {
//readMoreOpen: false,
expandedCard: null,
}
this.readMore = this.readMore.bind(this);
}
readMore(article) {
//this.setState({ readMoreOpen: !this.state.readMoreOpen })
this.setState({expandedCard: article})
}
render() {
const { articles } = this.props;
const { expandedCard } = this.state;
return (
articles.map(article => {
// the look of each card depends on state.expandedCard only if article == expandedCard it's shown with 'openFullParagraph' class
return (
<div className="projectCardRoot" key={article.id}>
<div className="projectCardMainLogin">
<div className="projectCardMiddle">
<p className={article == expandedCard ? 'openFullParagraph' : 'closeFullParagraph'} id="projectCardDesc">{article.description}</p>
<div className="cardReadMore desktopDiv" onClick={() => this.readMore(article)}>Read more</div>
</div>
</div>
</div>
)
})
)
}
}
export default BidCard;

How to remove a button if there is no more info in the api

The following code is running smoothly, but I want to implement it in a way that when I do a getNextPers() and there is no info, it hides/removes the Ver Mais button. I've been looking for solutions but have found none, so any help is good. Thank you.
import React from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';
class List extends React.Component {
constructor(props){
super(props);
this.state = {
personagens: [],
page: 1,
showBtn: true,
};
this.getNextPers = this.getNextPers.bind(this);
}
getNextPers(){
const peopleApiEndpoint = `https://swapi.co/api/people/${this.state.page}`;
axios.get(peopleApiEndpoint).then((p) =>
if(p=={}){
this.setState({ showBtn: false });
}
else {
this.setState({ personagens: this.state.personagens.concat(p), page: this.state.page+1 })
}
);
}
render(){
return (
<div>
<p><b>Personagens:</b></p>
{this.state.personagens.map((pers, i) => (
<div key={i}>
<br />
<p><i>Name:</i> {pers.data.name}</p>
<p><i>Height:</i> {pers.data.height} cm</p>
<p><i>Mass:</i> {pers.data.mass} kg</p>
</div>
))}
<button onClick={this.getNextPers}>Ver Mais</button>
</div>
);
}
}
ReactDOM.render(<List />, document.getElementById('root'));
The real problem is here:
axios.get(peopleApiEndpoint).then((p) => {
if (p == {}) { // THIS WILL NEWER WORK AS EXPECTED
this.setState({showBtn: false});
} else {
this.setState({
personagens: this.state.personagens.concat(p),
page: this.state.page + 1
});
}
});
Also swapi return 404 when there is no more results instead of empty object so you need to add catch block to your axios.get as described in docs: https://github.com/axios/axios#handling-errors
axios.get(peopleApiEndpoint).then((p) => {
this.setState({
personagens: this.state.personagens.concat(p),
page: this.state.page + 1
});
}).catch((err) => {
this.setState({showBtn: false});
});
Now you can use conditional rendering like:
{(this.state.showBtn && <button onClick={this.getNextPers}>Ver Mais</button>)}
First thing getNextPers does not return anything and you can achieve the show/hide by using condintion in your code
{ this.your_condition ?
<button onClick={this.getNextPers}>Ver Mais</button> : ''
}
As addition to Ramya answer you can also use
render(){
return (
<div>
<p><b>Personagens:</b></p>
{this.state.personagens.map((pers, i) => (
<div key={i}>
<br />
<p><i>Name:</i> {pers.data.name}</p>
<p><i>Height:</i> {pers.data.height} cm</p>
<p><i>Mass:</i> {pers.data.mass} kg</p>
</div>
))}
{ this.state.showBtn && <button onClick={this.getNextPers}>Ver Mais</button> }
</div>
);
}
Since you're storing the showBtn state in your component, you can use it to conditionally render the button as follows:
render(){
return (
<div>
<p><b>Personagens:</b></p>
{this.state.personagens.map((pers, i) => (
<div key={i}>
<br />
<p><i>Name:</i> {pers.data.name}</p>
<p><i>Height:</i> {pers.data.height} cm</p>
<p><i>Mass:</i> {pers.data.mass} kg</p>
</div>
))}
{ (this.state.showBtn) ?
<button onClick={this.getNextPers}>Ver Mais</button>
:
null
}
</div>
);
}

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

Categories

Resources