Toggle one element in React.js onClick - reactjs

I have info button that is supposed to open specific description element onclick event - info is obtained from Firebase. However, myOnclick event triggers all of the siblings elements and I need to toggle/untoggle only specific one. What am I missing and doing wrong?
here's the code:
import React, { Component } from "react";
import firebase from "../../firebase";
//Data obtained from DB and rendered on page
export default class Tour extends Component {
constructor() {
super();
this.state = {
tours: [],
showInfo: false,
};
}
// button that toggles info
handleInfo = () => {
this.setState({
showInfo: !this.state.showInfo,
});
};
// component did mount
componentDidMount() {
const dbRef = firebase.database().ref();
dbRef.on("value", (snapshot) => {
// checking changes in db
const data = snapshot.val();
const newToursAarray = [];
for (let inventoryName in data) {
const toursObject = {
id: inventoryName,
tours: data[inventoryName],
name: data[inventoryName].name,
seats: data[inventoryName].seats,
date: data[inventoryName].date,
duration: data[inventoryName].duration,
imgUrl:"https://source.unsplash.com/350x350/?" + data[inventoryName].name,
// temporary tour info placeholder and will be removed and connetcted to real DB
info: "Lorem ipsum dolora saepe fugiat. " +
data[inventoryName].name,
};
newToursAarray.push(toursObject);
}
this.setState({
tours: newToursAarray,
});
});
}
render() {
return (
<div className="tourlist">
{this.state.tours.map((toursObject) => {
return (
<section className="tourItem">
<header>
<h3> {toursObject.name} </h3>
<h5>
info
{/* button that toggles info */}
<span onClick={this.handleInfo}>
<i className="fas fa-caret-square-down"></i>
</span>
</h5>
</header>
<ul className="inventoryItem" key={toursObject.id}>
<li> {toursObject.date} |</li>
<li>{toursObject.duration} hrs |</li>
<li> {toursObject.seats} seats </li>
</ul>
<div className="img-container">
{this.state.showInfo && (
// text that toggles when clicking on info button
<p className="tour-info">{toursObject.info}</p>
)}
<img src={toursObject.imgUrl} alt="image of the tour" />
</div>
</section>
);
})}
</div>
);
}
}

So the issue here is that the expanding options are all referencing the same piece of state. You have your state.showInfo outside of your tours.map. So as you map through your tours, you say "if 'showInfo' then reveal this section" and that 'showInfo' is the same for everything.
What I would recommend is storing the id of the expanded tourObject, and then you can check against that.
Something like this:
handleInfo = (id) => {
this.setState({
infoToShow: id,
});
};
And then in your onClick it would look more like this:
<span onClick={() => this.handleInfo(tourObject.id)}>
<i className="fas fa-caret-square-down"></i>
</span>
And your logic to show or hide can just be this:
{this.state.infoToShow === tourObject.id && (
// text that toggles when clicking on info button
<p className="tour-info">{toursObject.info}</p>
)}
This way you can store the id in a place accessible to all the looped through tours, but they won't conflict with each other.
So instead of checking if showInfo is true, check if the id that you want to show matches the id of the tour.

Related

Unable to SetState in React Class using ReactTable Custom Header

I'm running into an issue with setState and I'm not exactly sure what I'm doing incorrectly.
I'll provide the full code below.
I start off with a function class to call my API to pull data from the database, then pass this to my Applicants Class. The Applicants Class is a datatable with the far right column being an edit button to bring a modal popup which will include a form to update any applicants in my table. (image below)
As you can see from my imports, I'm using ReactTable v7.7, so I'm able to set custom headers in my datatable. I'm trying to add a button that calls my classes ToggleModal function. This will set the modalDisplayBool to true (so the modal is visible) and then pass in that rows data. When the Modal is visible, I have a button to close it which calls the same ToggleModal function to hide the modal and reset the rows data.
My Modal 'X' button to close the modal works perfectly fine. It is the 'Edit' button in my Header that doesn't seem to work. When I click this, I receive the following error in my console:
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 to 'this.state' directly or define a 'state = {};' class property with the desired state in the Applicants component.
PS. I've also tried binding the class function in the constructor and binding in the onClick, but this doesn't change anything that I can see. Additionally, since I'm using React v17, it's not advised to use ComponentDidMount or any of the sort as they are antipatterns. However, I will note that I tried using the SAFE_ version, and this still didn't change my out come.
import React from 'react'
import { useApi, handleHttpServiceCallback } from '../common/services/HttpServices'
import { ReactTableBasicApplicantsPage } from '../common/datatable/ReactTable'
import Modal from '../common/modal/Modal'
import Button from '#material-ui/core/Button'
export default function ApplicantsPage() {
const url = '/users/applicants'
const { data, isLoading, hasError, httpStatus, redirectURL } = useApi(url, {initialLoad: true}, { credentials: 'include' })
var responseFromCallback = handleHttpServiceCallback(data, isLoading, hasError, httpStatus, redirectURL)
if(responseFromCallback)
return responseFromCallback
return (
<Applicants data={data}/>
)
}
class Applicants extends React.Component {
constructor(props) {
super(props)
this.state = {
modalDisplayBool: true, // Will be false initially, but I can't get this value to be true when clicking the 'Edit' button, so setting it to true for now to test
selectedRowData: {},
data: props.data
}
const headerCol = {
Header: () => null, // No header
id: 'edit_applicant',
sortable: false,
filterable: false,
Cell: ({ row }) => (
<span>
<Button variant="outlined" color="primary" onClick={() => this.toggleModal(row.original)}> // This Doesn't Work
Edit
</Button>
</span>
)
}
const headerFound = this.state.data.column.some(x => x.id === headerCol.id)
if(!headerFound)
this.state.data.column.push(headerCol)
}
toggleModal = (selectedRowData) => {
this.setState({
modalDisplayBool: !this.state.modalDisplayBool,
selectedRowData
})
}
render() {
return (
<>
<h1>Applicants Page</h1>
<ReactTableBasicApplicantsPage columns={this.state.data.column} dataIn={this.state.data.data}/>
<Modal show={this.state.modalDisplayBool}>
<div className="ttp-modal-main">
<div className="ttp-modal-header">
<h1>Modal Header</h1>
<button
type="button"
className="btn btn-danger"
onClick={() =>this.toggleModal({})} // This Works
>
<span aria-hidden="true">×</span>
</button>
</div>
<section className="ttp-modal-content">
<p>This is the main content of the modal</p>
</section>
<section className="ttp-modal-footer">
<div>Footer area</div>
</section>
</div>
</Modal>
</>
)
}
}
With help from #vishnu-shekhawat, I fixed my issue.
As mentioned in their comment, I simply moved my headerCol variable into my classes render. See code below
class Applicants extends React.Component {
constructor(props) {
super(props)
this.state = {
modalDisplayBool: true,
selectedRowData: {},
data: props.data
}
}
toggleModal = (selectedRowData) => {
this.setState({
modalDisplayBool: !this.state.modalDisplayBool,
selectedRowData
})
}
render() {
// vvvv Moving this here fixed my issue
const headerCol = {
Header: () => null, // No header
id: 'edit_applicant',
sortable: false,
filterable: false,
Cell: ({ row }) => (
<span>
<Button variant="outlined" color="primary" onClick={() => this.toggleModal(row.original)}>
Edit
</Button>
</span>
)
}
const headerFound = this.state.data.column.some(x => x.id === headerCol.id)
if(!headerFound)
this.state.data.column.push(headerCol)
return (
<>
<h1>Applicants Page</h1>
<ReactTableBasicApplicantsPage columns={this.state.data.column} dataIn={this.state.data.data} />
<Modal show={this.state.modalDisplayBool}>
<div className="ttp-modal-main">
<div className="ttp-modal-header">
<h1>Modal Header</h1>
<button
type="button"
className="btn btn-danger"
onClick={() =>this.toggleModal({})}
>
<span aria-hidden="true">×</span>
</button>
</div>
<section className="ttp-modal-content">
<p>This is the main content of the modal</p>
</section>
<section className="ttp-modal-footer">
<div>Footer area</div>
</section>
</div>
</Modal>
</>
)
}
}

How do I add in an additional property for each individual mapped item after already making a call to a 3rd party API?

I want to add a feature that increments how many "likes" someone gets similar to FB. The profiles are getting passed in through an Axios GET request through a 3rd party API. When a user clicks on the like button, the amount of likes someone gets should increment by 1. The code I previously wrote in handleClicks() increments everyone's likes by 1 rather than just one individual person. The data is passed into cards[] in one chunk.
App.js
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
class App extends React.Component {
constructor(props) {
super(props)
this.state = {cards: [], numVotes: 0};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log("This is working!");
this.setState(numVotes: state.numVotes + 1})
}
componentDidMount() {
axios.get('/')
.then(res => {
this.setState({cards: res.data})
console.log(this.state);
})
}
render() {
return (
<div className="main-container">
<Header />
<CardList
cards={this.state.cards}
handleClick={this.handleClick}
/>
<hr className="ui divider"></hr>
</div>
);
}
export default App;
const CardList = props => {
const cards = props.cards.map(card => {
return <Card image={card.image_url}
name={card.name}
title={card.title}
blurb={card.bio}
handleClick={props.handleClick}
numVotes={props.numVotes}
/>
})
return <div className="ui divided items">
{cards}
</div>
}
Card.js
const Card = (props) => {
return (
<div className="card-component item">
<div class="ui small rounded image">
<img className="portrait"
src = {props.image}
onError={(e)=>{e.target.onerror = null; e.target.src='https://image.shutterstock.com/image-vector/no-image-available-vector-illustration-260nw-744886198.jpg'}}
/>
</div>
<div class="content">
<a className="header">{props.name}</a>
<div class="meta">
<span className="title">{props.title}</span>
</div>
<p className="blurb">{props.blurb}</p>
<p><span className="question"> Want to work with {props.name}?</span>
<span className="like-button" onClick={props.handleClick}>
<img className="icon" src={icon} />Yes!
</span>
</p>
<p className="yes-amt">{props.numVotes} people have said Yes!</p>
</div>
</div>
)
}
You need to first decide how to identify each card as unique so you can update the correct one. If you have an id that would be ideal, but I'm going to assume the name is unique since its a value in your question.
// pass the unique identifier to handler
handleClick(name) {
this.setState((prevState) => ({
// map over the previous cards and return a new array
cards: prevState.cards.map((card) => {
// If the name matches the current card, change it
if (card.name === name) {
return {...card, numVotes: card.numVotes + 1};
} else {
// Otherwise just return the same card unchanged.
return card;
}
})
}))
}
Then in your component use it like this:
// Use inline function so we can pass it a prop as a parameter
<span className="like-button" onClick={() => props.handleClick(props.name)}>

Map creates only one li - React

I'm making a component that creates Tabs when you click on an item in the navigation menu.
All the controls I do in the father to be able to pass the states between the brothers without problems.
As it is now my code when clicking on a menu item creates a unique <li> and shows the name. If you click on the entire menu item, it does not create a new <li>, but rather updates the old one with the new menu information.
I need that every time I press a menu item a new <li> with its content is created.
I edit my code with #technogeek1995 changes and this the final solution:
class App extends Component {
constructor(props, context){
super(props, context);
["openTabs",].forEach((method) => {
this[method] = this[method].bind(this);
});
this.state = {
navigation: {
menu: [],
},
tabs:{
tabsLi:[],
},
textvalue : "",
showtabs: true,
}
}
componentDidMount() {
fetch('json_menu.php')
.then(response => response.json())
.then(data =>{
this.setState({navigation: data});
//console.log(data)
})
}
openTabs(e, url, iframe, trdtitle){
e.preventDefault();
const state = {...this.state};
state.textvalue = trdtitle.split();
state.tabs.tabsLi.push(state.textvalue);
console.log(state.tabs.tabsLi)
this.setState({ state });
this.setState({
showtabs: false,
});
}
class Tabs extends Component {
render(){
const renderTabs = tabs =>{
return(
<div id="content-tabs" className="tabs">
{( this.props.showtabs)
? (
<>
<div className="waiting-leads">
<p>Parece que todavía no hay ningún lead...</p>
<h3>¡Ánimo, ya llega!</h3>
<img src={imgDinosaurio} alt="Dinosaurio"></img>
</div>
</>
) : (
<ul id="resizable" className="content" >
{this.props.tabs.tabsLi.map((value, index) => {
return (
<li key={index}>
<span>{value}</span>
<Icon icon="cerrar" className='ico-cerrar' onClick={remove_tab(index)}/>
</li>
)
})}
</ul>
)}
</div>
);
}
return (
<>
{renderTabs(this.props.tabs.tabsLi)}
</>
)
}
}
This is the code that is generated when you click on the menu item. The <span> is the one that is updated and no new <li> is created. Ventas is my element menu name.
<ul id="resizable" class="content">
<li>
<span>Ventas</span>
<svg class="ico-cerrar">path</svg>
</li>
</ul>
The issue appears to be related to mutating the state directly. You should see some warnings in the console/terminal about mutating react's state directly. I have updatd your openTabs function so that it no longer mutates the state directly. Instead, I copy state to a local variable, perform the mutations of the local state object. Then, I call setState with the locally updated state object. React will automatically pick up the changes to the state and render the page with the (newly) updated state.
openTabs(e, url, iframe, trdtitle){
e.preventDefault();
const state = {...this.state};
state.textvalue = trdtitle.split();
state.navigation.menu.push(state.textvalue);
state.showtabs = false;
this.setState({ state });
}
Tabs Component needed to be updated to iterate over the list, rather than over the string so it will create a <li> for every element in state.navigation.menu. remove_tab needed to be wrapped in {} instead of "" as well.
class Tabs extends Component {
render() {
return ( <
div id = "content-tabs"
className = "tabs" > {
(this.props.showTabs) ? (
<div className = "waiting-leads" >
<p> Parece que todavía no hay ningún lead... </p>
<h3> ¡Ánimo, ya llega! </h3>
<img src={imgDinosaurio} alt="Dinosaurio"/>
</div>
) : (
<ul id = "resizable" className = "content" >
{this.props.tabs.map((value, index) => (
<li key={index} >
<span>{value}</span>
<Icon icon = "cerrar" className = 'ico-cerrar' onClick={remove_tab(index)} / >
</li>
)
} </ul>
)} </div>
);
}
}
You should use the callback version of state and use the spread syntax to create new objects with new references so that React detects the change in state.
this.state.navigation.menu.push(this.state.textvalue)
Also, this line will push the old textValue and not the new one which is trdtitle.split()
openTabs(e, url, iframe, trdtitle){
e.preventDefault();
const textValue = trdtitle.split()
this.setState(state => ({
textvalue,
showtabs: false,
navigation: {
...state.navigation,
menu: [ ...state.navigation.menu, textValue ]
}
}));
}

How to render reusable component one at a time in reactjs?

I reuse the Chat component twice into another component. It display when you click Chat button but it overlaps each other.
class Chat extends React.Component {
constructor() {
super();
this.state = {
show: false,
};
}
reset = () => {
this.setState(false);
}
open = () => {
this.setState({ show: true });
}
close = () => this.setState({ show: false });
render() {
return (<div className="chat">
<button className="btn-yes round" onClick={this.open}>{this.props.title}</button>
{this.state.show &&
<div className="show-chat">
<div className="chat-head">Now Chatting <i className="fas fa-angle-down" onClick={this.close}></i></div>
<div className="chat-body">
<div className="blue">Teresa wants to chat about her healthcare finances</div>
<ul>
<li><img src={agentPhoto} alt="chat agent avatar" /></li>
<li>
<h6>John Newman</h6>
<div className="gray">Hi Teresa!</div>
<div className="gray">Here is the link to the finance tool we discussed.</div>
<div className="gray">If you have any questions, let me know!</div>
</li>
</ul>
</div>
<input placeholder="Type here and hit enter to chat"></input>
</div>}
</div>);
}
}
I expect to display chat one at a time. When I click the Chat button 2 and the Chat 1 is displayed, Chat 1 should be hidden.
Essentially, you need to give each Chat component an identifier and keep track of the one that is currently opened.
Here is the basic structure for your Parent component:
class App extends React.Component {
state = {
currentChatId: null
};
handleOpen = id => {
this.setState({
currentChatId: id
});
};
render() {
return (
<div>
<Chat
identifier={1}
currentChatId={this.state.currentChatId}
handleOpen={this.handleOpen}
/>
<Chat
identifier={2}
currentChatId={this.state.currentChatId}
handleOpen={this.handleOpen}
/>
</div>
);
}
}
So notice, we give each Chat component an identifier prop. We will use identifier to update the active chat - which we stored as a value called currentChatId in our parent-state. That is all done through the handleOpen() event-handler, which we also pass down as a prop to Chat.
Now in your Chat component, we need to configure logic for open() and componentDidUpdate()
class Chat extends React.Component {
constructor() {
super();
this.state = {
show: false
};
}
componentDidUpdate(prevProps) {
const { identifier, currentChatId } = this.props;
if (this.props.currentChatId !== prevProps.currentChatId) {
this.setState({
show: identifier === currentChatId ? true : false
});
}
}
open = () => {
const { identifier, handleOpen } = this.props;
handleOpen(identifier);
};
render() {
return (
<div className="chat">
<button className="btn-yes round" onClick={this.open}>
{this.props.title}
</button>
{this.state.show && (
<div className="show-chat">
<div className="chat-head">
Now Chatting{" "}
<i className="fas fa-angle-down" onClick={this.close} />
</div>
<div className="chat-body">
<div className="blue">
Teresa wants to chat about her healthcare finances
</div>
<ul>
<li>
<img src={""} alt="chat agent avatar" />
</li>
<li>
<h6>John Newman</h6>
<div className="gray">Hi Teresa!</div>
<div className="gray">
Here is the link to the finance tool we
discussed.
</div>
<div className="gray">
If you have any questions, let me know!
</div>
</li>
</ul>
</div>
<input placeholder="Type here and hit enter to chat" />
</div>
)}
</div>
);
}
}
Workflow:
User clicks one of the Chat buttons, triggering handleOpen()and we
pass in the unique identifier....
That gets passed back up to the Parent, and now currentChatId
should be updated with the identifier...
That currentChatId gets passed back down to the Chat component as the
currentChatId prop...
Triggers componentDidUpdate() on all Chat components, and we check
the currentChatId against their own identifiers, only one will be
matching, so we display that one.
See codesandbox for working example: https://codesandbox.io/s/react-example-kgm2h

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