React js - Array doesn't update after a onSwipe() event - javascript

I am a Beginner to Reactjs and I just started working on a Tinder Clone with swipe functionality using tinde-card-react.
I am trying to get two variables to update using React useState() but coudn't.
There are 2 main components inside the main function, a TinderCards component and Swipe right and left and Replay buttons. The problem is that when I swipe the cards manually variables don't get updated and this is not the case when i swipe using the buttons.
In the current log, I swiped the cards twice to the right and logged the variables alreadyRemoved and people. The variable people is initially an Array containing 3 objects so after the second swipe it's supposed to log only 2 objects not 3, While the alreadyRemoved variable is supposed to update to the missing elements of the variable people.
This is my code :
import React, { useState, useEffect, useMemo } from 'react';
import './IslamCards.css';
import Cards from 'react-tinder-card';
import database from './firebase';
import hate from "./Cross.png"
import replayb from "./Replay.png"
import love from "./Love.png"
import IconButton from "#material-ui/core/IconButton"
function IslamCards(props) {
let [people, setPeople] = useState([])
useEffect(() => {
database.collection("People").onSnapshot(snapshot => { setPeople(snapshot.docs.map(doc => doc.data())) })
}, [])
let [alreadyRemoved , setalreadyRemoved] = useState([])
let buttonClicked = "not clicked"
// This fixes issues with updating characters state forcing it to use the current state and not the state that was active when the card was created.
let childRefs = useMemo(() => Array(people.length).fill(0).map(() => React.createRef()), [people.length])
let swiped = () => {
if(buttonClicked!=="clicked"){
console.log("swiped but not clicked")
if(people.length){
let cardsLeft = people.filter(person => !alreadyRemoved.includes(person))
if (cardsLeft.length) {
let toBeRemoved = cardsLeft[cardsLeft.length - 1] // Find the card object to be removed
let index = people.map(person => person.name).indexOf(toBeRemoved.name)// Find the index of which to make the reference to
setalreadyRemoved(list => [...list, toBeRemoved])
setPeople(people.filter((_, personIndex) => personIndex !== index))
console.log(people)
console.log(alreadyRemoved)
}
}
buttonClicked="not clicked"
}
}
let swipe = (dir) => {
buttonClicked="clicked"
console.log("clicked but not swiped")
if(people.length){
let cardsLeft = people.filter(person => !alreadyRemoved.includes(person))
if (cardsLeft.length) {
let toBeRemoved = cardsLeft[cardsLeft.length - 1] // Find the card object to be removed
let index = people.map(person => person.name).indexOf(toBeRemoved.name)// Find the index of which to make the reference to
setalreadyRemoved(list => [...list, toBeRemoved])
childRefs[index].current.swipe(dir)
let timer =setTimeout(function () {
setPeople(people.filter((_, personIndex) => personIndex !== index))}
, 1000)
console.log(people)
console.log(alreadyRemoved)
}
// Swipe the card!
}
}
let replay = () => {
let cardsremoved = alreadyRemoved
console.log(cardsremoved)
if (cardsremoved.length) {
let toBeReset = cardsremoved[cardsremoved.length - 1] // Find the card object to be reset
console.log(toBeReset)
setalreadyRemoved(alreadyRemoved.filter((_, personIndex) => personIndex !== (alreadyRemoved.length-1)))
if (!alreadyRemoved.length===0){ alreadyRemoved=[]}
let newPeople = people.concat(toBeReset)
setPeople(newPeople)
// Make sure the next card gets removed next time if this card do not have time to exit the screen
}
}
return (
<div>
<div className="cardContainer">
{people.map((person, index) => {
return (
<Cards ref={childRefs[index]} onSwipe={swiped} className="swipe" key={index} preventSwipe={['up', 'down']}>
<div style={{ backgroundImage: `url(${person.url})` }} className="Cards">
<h3>{person.name}</h3>
</div>
</Cards>);
})}
</div>
<div className="reactionButtons">
<IconButton onClick={() => swipe('left')}>
<img id="hateButton" alt="d" src={hate} style={{ width: "10vh", marginBottom: "5vh", pointerEvents: "all" }} />
</IconButton>
<IconButton onClick={() => replay()}>
<img id="replayButton" alt="e" src={replayb} style={{ width: "11vh", marginBottom: "5vh", pointerEvents: "all" }} />
</IconButton>
<IconButton onClick={() => swipe('right')}>
<img id="loveButton" alt="f" src={love} style={{ width: "11vh", marginBottom: "5vh", pointerEvents: "all" }} />
</IconButton>
</div>
</div>
);
}
export default IslamCards;
My console Log :
UPDATE :
As suggested in the 1st answer, I removed the Timer from the swiped() function but the problem persisted.
I hope to get more suggestions, so that I can solve this problem.

I can see the problem, but you might need to figure out what to do after that.
setPeople(people.filter((_, personIndex) => personIndex !== index))}
, 1000)
The problem is that index is figured out from the current update, however it takes 1 second to reach the next update, in between, your index points to the same one, because your index is derived from the people.

Related

React Stepper with dynamic and responsive steps

I'm building a React stepper with MUI and I wanted to have it be dynamic depending on the data coming in and also to be able to add/remove steps within that dynamic section. I was not able to find any examples or posts on here regarding what I had in mind, and the docs for MUI's stepper don't go anywhere near touching dynamic/responsive steps like this. I've been able to get it pretty far (actually a bit surprised I got it as far as I have), but I'm stuck right at the end. I've set up a generic example on CSB (link below), with the stepper pulling in data objects, displaying them on the dynamic steps, and the add/remove functionality works for the step labels, but not for the content. This is where I can't figure it out, I set up the structure the same for the labels and content, and since is basically just pushing to/filtering out arrays, I'm not finding where the difference is. I figure I'd post it here before messing with it any further. Also, if anyone has any suggestions on how to clean it up, I'd be happy to hear those as well, I'm sure it is a bit messy and heavy handed in getting the job done.
Heres the main Stepper component:
const DynamicStepper = () => {
const [open, setOpen] = useState(false);
const [activeStep, setActiveStep] = useState(0);
const [copiedObjs, setCopiedObjs] = useState([]);
const [copiedLabels, setCopiedLabels] = useState([]);
const [middleContent, setMiddleContent] = useState([]);
const [stepsContent, setStepsContent] = useState([])
//Dialog actions
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
//Stepper actions
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
}
//Setting up dynamic labels (actual code involves conditional api calls)
useEffect(() => {
if(data) {
setCopiedObjs(data)
}
}, [])
useEffect(() => {
if(copiedObjs.length) {
let labeling = copiedObjs.map((i) => (
i.title
))
setCopiedLabels(labeling)
}
}, [copiedObjs])
//Set labels for stepper
const initialLabels = ["First", "Second", "Third"]
const finalLabels = ["One from the End", "Final"]
const steps = initialLabels.concat(copiedLabels).concat(finalLabels)
//Set content for stepper
//moved into useEffect
// function getMiddleContent(){
// const content = copiedObjs.map((obj, idx) => (
// <StepDynamic key={idx} props={obj} />
// ))
// setMiddleContent(content)
// }
useEffect(() => {
const content = copiedObjs.map((obj, idx) => (
<StepDynamic key={idx} props={obj} />
))
setMiddleContent(content)
}, [middleContent, copiedObjs])
useEffect(() => {
//add/delete steps
function addStep(n = 1){
let newSteps = [...copiedLabels];
let newContent = [...middleContent];
let newLabel = ["new obj"]
newSteps.push(...newLabel);
console.log("midContent pre push: ", middleContent)
let content = [<StepDynamic key={Math.random()*3} props={null} />]
newContent.push(...content)
console.log("postPush, newContent: ", content)
setCopiedLabels(newSteps)
setMiddleContent(content)
}
function removeStep(idx){
let newSteps = [...copiedLabels];
let newContent = [...middleContent];
let steps = newSteps.filter((item, i) => i !== idx);
let content = newContent.filter((item, i) => i !== idx)
setCopiedLabels(steps)
setMiddleContent(content)
}
const initialContent = [<StepOne key={1} />, <StepTwo key={2} />, <StepThree key={3} addStep={addStep} removeStep={removeStep} titles={copiedLabels} />]
const finalContent = [<StepPenUltimate key={4} />, <StepFinal key={5} />]
const content = initialContent.concat(middleContent).concat(finalContent)
setStepsContent(content)
}, [middleContent, copiedLabels])
function getStepsContent(stepIndex) {
return stepsContent[stepIndex]
}
//Moved this section into useEffect to see if I got better results, but responds the same as before
// //add/delete steps
// function addStep(n = 1){
// let newSteps = [...copiedLabels];
// let newContent = [...middleContent];
// let newLabel = ["new obj"]
// newSteps.push(...newLabel);
// console.log("midContent pre push: ", middleContent)
// let content = [<StepDynamic key={Math.random()*3} props={null} />]
// newContent.push(...content)
// console.log("postPush, newContent: ", content)
// setCopiedLabels(newSteps)
// setMiddleContent(content)
// }
// function removeStep(idx){
// let newSteps = [...copiedLabels];
// let newContent = [...middleContent];
// let steps = newSteps.filter((item, i) => i !== idx);
// let content = newContent.filter((item, i) => i !== idx)
// setCopiedLabels(steps)
// setMiddleContent(content)
// }
return (
<>
<Button variant="contained" color="primary" onClick={handleClickOpen}>
New Stepper
</Button>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>Stepper Guide</DialogTitle>
<DialogContent>
<DialogContentText>
Just some words of guidance
</DialogContentText>
<div>
<Stepper activeStep={activeStep} alternativeLabel>
{steps && steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
<br />
</Stepper>
<>
{activeStep === steps.length ? (
"Complete"
) : (
<>
{getStepsContent(activeStep)}
<Button color='warning' onClick={handleBack}>
{activeStep === steps[0] ? "" : "Back"}
</Button>
<Button color="primary" onClick={handleNext}>
{activeStep === steps.length ? "Submit" : "Next"}
</Button>
</>
)}
</>
</div>
<DialogActions>
<Button color="error" onClick={handleClose}>
Cancel
</Button>
</DialogActions>
</DialogContent>
</Dialog>
</>
)
}
And heres the CSB: https://codesandbox.io/s/eager-cerf-tsrbci?file=/src/Stepper.js:463-5442
UPDATE
After playing around and a lot of console.logs I think I've figured out where the issue is, but I'm not sure how to resolve it. When adding/ removing a step, the functions are wrapped within the same useEffect as the logic to set the content for the steps. This was done to be able to provide the functions to the StepThree component where the adding/ removing takes place on the UI. However, it seems the state update and the logic setting the content are not on the same time cycle and so the content setting does not have access to the new updated state. I've tried breaking apart the useEffect into multiple and tried just writing separate functions, but because there is a lot of conditional state being utilized, and in order for the different pieces to correctly have access, as well as to avoid infinite re-render loops, I keep coming back to the same setup. The label setting works fine because the setting of the array for the labels happens outside the useEffect that compiles the conditional state.
If I'm wrong on my assessment please let me know, and if you have any ideas on how to better structure this, I'd love to hear it. I've looked at useReducer as a possible alternative, but I'm not that familiar with the react hooks beyond useState and useEffect, and so far haven't determined how to write out the reducer, again I keep coming back to needing to have the conditional pieces of state available at the right place in the code.

REACT Duplicated Key Warning

I am currently get this duplicated key warning "Warning: Encountered two children with the same key". However, I am unsure where this duplication of key comes from. I am using the fileData id as my key which should be unique as it is firebase generated id. Therefore, I am not so sure what is happening behind here.
Here are my codes below and the warning I get.
MultimediaDetails.js
import React, { useEffect, useState } from "react";
import * as AiIcons from "react-icons/ai";
import * as FaIcons from "react-icons/fa";
import { database } from "../../../firebase";
import ViewImageFileModal from "../../modals/multimediaModals/view/ViewImageFileModal";
/**
* It's a component that displays audio, video, and image files
* #param props - The props object that is passed to the component.
* #returns The MultimediaDetails component is being returned.
*/
const MultimediaDetails = (props) => {
/* Destructuring the props object. */
const { pId } = props;
/* Setting the state of the component. */
const [imageData, setImageData] = useState([]);
const [imageMessage, setImageMessage] = useState(true);
const userType = JSON.parse(localStorage.getItem("admin") ?? false);
// Modal Variables
const [showViewImageModal, setShowViewImageModal] = useState(false);
const [fileData, setFileData] = useState(Object);
/**
* When the user clicks on the audio, video, or image file, the file data is set and the modal is
* toggled.
* #param obj
*/
const viewImageFile = (obj) => {
setFileData(obj);
toggleViewImageModal();
};
/* The function to toggle modal states */
const toggleAddImageModal = () => setShowAddImageModal((p) => !p);
const toggleViewImageModal = () => setShowViewImageModal((p) => !p);
useEffect(() => {
/* Query data from database and listening for changes. */
const imageQuery = database.portfolioRef.doc(pId).collection("images");
const unsubscribeImage = imageQuery.onSnapshot((snapshot) => {
if (snapshot.docs.length !== 0) {
setImageMessage(false);
setImageData(
snapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id }))
);
} else {
setImageMessage(true);
}
});
return () => {
unsubscribeImage();
};
}, [pId]);
return (
<div className="multimedia-section">
<div id="image-section">
<div id="image-header">
<h6>
<u>Images</u>
</h6>
{userType ? (
<button className="addbtn" onClick={() => toggleAddImageModal()}>
<AiIcons.AiOutlinePlus /> Add Image File
</button>
) : (
<></>
)}
</div>
<div id="image-content" className="multimedia-flex">
{imageMessage ? (
<p>There is not existing images for this portfolio.</p>
) : (
<div>
{imageData.map((doc) => (
<button
key={doc.id}
className="fileBtn"
onClick={() => viewImageFile(doc)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
</div>
)}
</div>
</div>
<ViewImageFileModal
show={showViewImageModal}
toggleModal={toggleViewImageModal}
pId={pId}
data={fileData}
key={fileData.id}
/>
</div>
);
};
export default MultimediaDetails;
The initialised values for the Modal.
/* Setting the initial state of the component. */
const valueState = {
name: '',
description: ''
}
const { currentUser } = useAuth();
const [formStateDisabled, setFormStateDisabled] = useState(true);
const [deleteState, setDeleteState] = useState(false);
const [message, setMessage] = useState('');
const [imageUrl, setImageUrl] = useState("");
const [loadForm, setLoadForm] = useState(false)
const [view, setView] = useState(false);
/* Destructuring the props object. */
const { show, toggleModal } = props;
const { handleChange, handleSubmit, values, errors, loading } =
useForm(validateUpdate, valueState, handleUpdate);
useEffect(() => {
if (Object.keys(props.data).length !== 0) {
values.name = props.data.imageName;
values.description = props.data.imageDesc;
setLoadForm(true);
}
}, [])
The warning I get (Shown Below), each time I click on the modal button to open the button, I noticed the warning actually repeats twice, and when I close it, it repeats another 2 times making it 4. I am not sure what is the cause of this, please help! Thank you!
Updates of trials
I only have 4 rows of data, all of which has its own unique id. Therefore I am unsure of where the duplicated key came from. However, if I remove the modal key "fileData.id" this warning would disappear. However, my component state will not reset and there will be a lot of props data issue that would surface. Where data for the previously clicked button will appear on the another button. Or the data might not appear at all.
FOR ADDITIONAL INFORMATION:
This is the output for the map buttons
I don't see any duplicates, and I am not sure where the issue is. Is there something I am doing wrong to cause this error. I checked my DB there isn't any data error as well.
Recommended solution
The problem is that your doc.id is repeating.
You are setting the imageData at the imageQuery.onSnapshot callback function, when you run the following code:
setImageData(snapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id })));
What you need to make sure is that doc.id is unique in this context (because you're using this value at the key attribute in your buttons).
That's the correct way to fix it.
Alternative solution
Another way to handle it (as a last resort), is using the following code, where you use the index position of the element at the key attribute:
{imageData.map((doc, index) => (
<button
key={index}
className="fileBtn"
onClick={() => viewImageFile(doc)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
But this is not recommended according to the React documentation:
We don’t recommend using indexes for keys if the order of items may
change. This can negatively impact performance and may cause issues
with component state. Check out Robin Pokorny’s article for an
in-depth explanation on the negative impacts of using an index as a key.
If you choose not to assign an explicit key to list items then
React will default to using indexes as keys.
Here is an in-depth explanation about why keys are necessary if you’re
interested in learning more.
Instead of doc.id use map item index like bellow. See if it works.
{imageData.map((index, doc) => (
<button
key={index}
className="fileBtn"
onClick={() => viewImageFile(doc)}>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
This is the culprit:
{imageData.map((doc) => (
<button
key={doc.id}
className="fileBtn"
onClick={() => viewImageFile(doc)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
The main issue here is that doc.id is duplicate, probably you have duplicate data in your imageData or ou have a faulty data in your database or something that generate a non-unique id.
To easily fix the issue, what you can do is use index of map.
{imageData.map((doc, index) => (
<button
key={index}
className="fileBtn"
onClick={() => viewImageFile(doc)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
index are always unique. but I suggest you should fix and see why you have duplicate data instead of just bypassing it with an index.
UPDATE
This is quite a hacky solution, but since I can't really pin point what's causing the issue without investigating first hand, let's make it so you don't have to pass a key on the modal.
So instead of storing the object data on the state, store the id instead:
Rename fileData to fileDataId.
const [fileDataId, setFileDataId] = useState(0);
then store the id when clicking the button.
{imageData.map((doc) => (
<button
key={doc.id}
className="fileBtn"
onClick={() => viewImageFile(doc.id)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
on the Modal, you have to pass the imageData and the selected id, then remove the key:
<ViewImageFileModal
show={showViewImageModal}
toggleModal={toggleViewImageModal}
pId={pId}
list={imageData}
selectedId={fileDataId}
/>
then inside ViewImageFileModal you can declare data as:
const data= props.list.find(image => image.id === props.selectedId);
I just wrap my model with a condition if imageId exist and it works already!
{imageId !== '' &&
<ViewImageFileModal
show={showViewImageModal}
toggleModal={toggleViewImageModal}
imageData={imageData}
key={imageId}
/>
}

Array not empty but length is 0 and JSON.stringify returns an empty list [duplicate]

This question already has answers here:
Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference
(7 answers)
console.log() async or sync?
(3 answers)
Closed 2 years ago.
I have been working on an event signup page for a school club I am in, and I cannot seem to have my program read the contents of my array, and put each one in a ListItem, which goes in a List for the life of me.
When I call this function, the boolean expression this.state.events.length !== 0 evaluates to False, however console.log(this.state.events) shows the array is not empty.
What might be causing this and how do you propose I go about fixing this.
Side note: I would love some criticism of my code. I just started with JS, React, and MaterialUI and would like to improve.
class EventSelector extends Component {
constructor(props){
super(props);
this.state = {
events: []
}
}
componentDidMount = () => {
var lst = [] // this is a list of events s.t. {key: "event_name"}
const eventsRef = db
.collection('events'); // this is a reference to the events collection
const offeredRef = eventsRef
.where('allowRegistration', '==', true)
.get() // this contains all docs which allow registration.
.then((querySnapshot) => {
querySnapshot.forEach((doc) => { // for each document, create key value pairs
lst.push({key: doc.data().name, id: doc.id})
})
})
// console.log(lst)
this.setState({ events: lst })
return true;
}
EventList = (events) => {
const { classes } = this.props;
return(
<div>
<Grid container item
direction='column'
alignItems='center'
justify='center'
style={{ maxHeight: '70vh', maxWidth: '50vw' }}
spacing={5}>
<Grid item>
<h1 className={classes.h1}>Upcoming Events</h1>
<h2 className={classes.h2}>Please select an event to sign up</h2>
</Grid>
<Grid container item direction='row' justify='center' spacing={5}>
<List component='b' subheader={<ListSubheader componenet='b'>Upcomming Events</ListSubheader>}>
{events.map(( {key , id} ) => {
// console.log(key)
return (
<div key={id}>
<ListItem button>
<ListItemText inset primary={key}/>
</ListItem>
</div>
);
}) }
</List>
</Grid>
</Grid>
</div>
);
}
// }
render = () => {
// const { classes, lists } = this.props;
const { classes } = this.props;
console.log(this.state.events)
var obj = Object.assign({}, this.state.events)
console.log(JSON.stringify(obj))
return (this.state.events.length !== 0 ? <h1>{JSON.stringify(this.state.events)}</h1> : <h2>Loading</h2>)
}
}
export default withStyles(styles)(EventSelector);
console output with Object.assign()
console output without Object.assign()
As far as I can see, there is nothing wrong with your code. One interesting thing about React, is that all your state changes will result into a new call to the render method.
state = { events: [] };
componentDidMount() {
// const lst = ...
this.setState({ events: lst });
// no need return here
};
render() {
console.log(this.state.events);
}
First time, it will print [] because your state is initialized this way. After the component is mounted, events will be print just like it was filled.
Another way to write this code is using a functional component:
import { useState } from 'react';
const EventSelector = () => {
const [events, setEvents] = useState([]);
useEffect(() => {
// const lst = ...
setEvents(lst);
}, []);
return events.length !== 0 ? (
<h1>{JSON.stringify(this.state.events)}</h1>
) : (
<h2>Loading</h2>
);
}
IMHO, functional components are better to read. This code works just as yours, the same behavior is expected.

Conditionally render part of object onClick inside a map (REACT.js)

I am trying to conditionally render part of an object (user comment) onClick of button.
The objects are being pulled from a Firebase Database.
I have multiple objects and want to only render comments for the Result component I click on.
The user comment is stored in the same object as all the other information such as name, date and ratings.
My original approach was to set a boolean value of false to each Result component and try to change this value to false but cannot seem to get it working.
Code and images attached below, any help would be greatly appreciated.
{
accumRating: 3.7
adheranceRating: 4
cleanRating: 2
date: "2020-10-10"
place: "PYGMALIAN"
staffRating: 5
timestamp: t {seconds: 1603315308, nanoseconds: 772000000}
userComment: "Bad"
viewComment: false
}
const results = props.data.map((item, index) => {
return (
<div className='Results' key={index}>
<span>{item.place}</span>
<span>{item.date}</span>
<Rating
name={'read-only'}
value={item.accumRating}
style={{
width: 'auto',
alignItems: 'center',
}}
/>
<button>i</button>
{/* <span>{item.userComment}</span> */}
</div >
)
})
You have to track individual state of each button toggle in that case.
The solution I think of is not the best but you could create a click handler for the button and adding a classname for the span then check if that class exists. If it exists then, just hide the comment.
Just make sure that the next sibling of the button is the target you want to hide/show
const toggleComment = (e) => {
const sibling = e.target.nextElementSibling;
sibling.classList.toggle('is-visible');
if (sibling.classList.contains('is-visible')) {
sibling.style.display = 'none'; // or set visibility to hidden
} else {
sibling.style.display = 'inline-block'; // or set visibility to visible
}
}
<button onClick={toggleComment}>i</button>
<span>{item.userComment}</span>
You can try like this:
const [backendData, setBackendData] = useState([]);
...
const showCommentsHandler = (viewComment, index) => {
let clonedBackendData = [...this.state.backendData];
clonedBackendData[index].viewComment = !viewComment;
setBackendData(clonedBackendData);
}
....
return(
<div>
....
<button onClick={() => showCommentsHandler(item.viewComment, index)}>i</button>
{item.viewComment && item.userComment}
<div>
You can store an array with that places which are clicked, for example:
const [ selectedItems, setSelectedItems] = React.useState([]);
const onClick = (el) => {
if (selectedItems.includes(el.place)) {
setSelectedItems(selectedItems.filter(e => e.place !== el.place));
} else {
setSelectedItems(selectedItems.concat(el));
}
}
and in your render function
const results = props.data.map((item, index) => {
return (
<div className='Results' key={index}>
<span>{item.place}</span>
<span>{item.date}</span>
<Rating
name={'read-only'}
value={item.accumRating}
style={{
width: 'auto',
alignItems: 'center',
}}
/>
<button onClick={() => onClick(item)}>i</button>
{ /* HERE */ }
{ selectedItems.includes(item.place) && <span>{item.userComment}</span> }
</div >
)
})
You need to use useState or your component won't update even if you change the property from false to true.
In order to do so you need an id since you might have more than one post.
(Actually you have a timestamp already, you can use that instead of an id.)
const [posts, setPosts] = useState([
{
id: 1,
accumRating: 3.7,
adheranceRating: 4,
cleanRating: 2,
date: "2020-10-10",
place: "PYGMALIAN",
staffRating: 5,
timestamp: { seconds: 1603315308, nanoseconds: 772000000 },
userComment: "Bad",
viewComment: false
}
]);
Create a function that updates the single property and then updates the state.
const handleClick = (id) => {
const singlePost = posts.findIndex((post) => post.id === id);
const newPosts = [...posts];
newPosts[singlePost] = {
...newPosts[singlePost],
viewComment: !newPosts[singlePost].viewComment
};
setPosts(newPosts);
};
Then you can conditionally render the comment.
return (
<div className="Results" key={index}>
<span>{item.place}</span>
<span>{item.date}</span>
<Rating
name={"read-only"}
value={item.accumRating}
style={{
width: "auto",
alignItems: "center"
}}
/>
<button onClick={() => handleClick(item.id)}>i</button>
{item.viewComment && <span>{item.userComment}</span>}
</div>
);
Check this codesandbox to see how it works.

How to create a load more button in Reactjs

I have cards and modals, I need to only show 2 cards in the page and have a button to show the rest, I’m new in programming and react, I don’t know what I have to do, that’s what I have now,
import React from "react"
import { Container } from "./_styles"
import { useTheme } from "#material-ui/core/styles"
import ImgMediaCard from "./../../../components/Cartao"
import AlertDialog from './../../../components/Modal'
export default function PortfolioSection(props) {
let arrayProjetos = props.projects;
const [selectedId, setSelectedId] = React.useState(0);
const [open, setOpen] = React.useState(false);
const handleClickOpen = (id) => {
setSelectedId(id);
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
let projetos = arrayProjetos.edges[selectedId].node.frontmatter;
return (
<Container>
{arrayProjetos.edges.map(
function criaCard(e, index){
let title = e.node.frontmatter.name;
let imageCard = e.node.frontmatter.images[0];
return (
<>
<ImgMediaCard
alt={title}
imagetitle={title}
texttitle={title}
src={imageCard}
click={() => handleClickOpen(index)}>
</ImgMediaCard>
</>
)
}
)}
<AlertDialog
imageModal={projetos.images[1]}
open={open}
handleClose={handleClose}
title={projetos.name}
text={projetos.description} />
</Container>
)
}
I'm using hooks to open the right modal when I click the "See more" button in the card, its working ok, I have 6 cards now, but I can add more in the future. I just need to limit how many cards I see when I enter the page and have a button to show everything.
API: you can add a pagination and total properties to your api call which returns 2 cards by default and you can handle the count of cards by increasing the pagination value. You may notice to add check to avoid situations like count > total.
UI: you can add const [cardCount, setCardCount] = useState(2)
and map through your cards array until the index not greater than cardCount value:
{arrayProjetos.edges.map((e, index) => { return index <= cardCount && <ImgMediaCard ... /> })}
<Box display={cardCount === 2 ? 'none' : 'block'}>
<Button
onClick={()=> arrayProjetos.edges.length - cardCount === 3 ? setCardCount(...cardCount - 1) : setCardCount(...cardCount - 2)}>See less</Button>
</Box>
<Box display={cardCount === arrayProjetos.edges.length ? 'none' : 'block'} >
<Button
onClick={() => arrayProjetos.edges.length - cardCount === 1 ? setCardCount(...cardCount + 1) : setCardCount(...cardCount + 2)}>See more </Button>
</Box>
How are you getting the cards?
You need to lazy load, if you are getting from a server, you can implement pagination, so the server sends back 2 cards every time (or based on a page data you send to server)
So every time you click the "load more" button, you fire a function who ask server for two more cards, and add the response to your cards js variable
Thanks Nazar, I did something similar I guess:
const [showMore, setShowMore] = React.useState(false);
const clickShow = () => {
setShowMore(oldValue => !oldValue);
showMore ? setButtonText("Ver Mais >") : setButtonText("Ver Menos <");
};
arrayProjetos.edges.slice(0, showMore ? arrayProjetos.edges.lenght : 2).map(
function criaCard(e, index){/*same thing here*/})
<button onClick={() => clickShow()}>{buttonText}</button>
I used slice to limit the array and a hook to change value

Categories