State is updated but component is not getting Re-rendered - javascript

I am using MUI card to display all the posts of logged in user. The MUI card has property of expand or collapse depending upon true or false. On each card I am giving key expanded which is set to empty string initially. On click of icon I am setting expanded of that particular post to non-empty string. If expanded field is non-empty string then I am setting true in collapse else false.
The state is getting updated as expected but component is not getting re-render and as a result expand or collapse is not working.
On initial render I am setting the post state and adding expanded field to it.
useEffect(() => {
let parr = [];
const unsub = database.posts
.orderBy("createdAt", "desc")
.onSnapshot((querySnapshot) => {
parr = [];
querySnapshot.forEach((doc) => {
let data = { ...doc.data(), postId: doc.id, expanded: "" };
parr.push(data);
});
setPosts(parr);
});
return unsub;
}, []);
Function called handleComment is responsible for setting expanded to empty string or non-empty string.
const handleComment = (id) => {
for (let i = 0; i < posts.length; i++) {
if (posts[i].pId === id) {
if (posts[i].expanded === "") posts[i].expanded = id;
else posts[i].expanded = "";
}
}
setPosts(posts);
console.log(posts);
};
A useEffect is set on posts which should be called whenever there is change posts state. But it is not getting called.
useEffect(() => {
console.log("re render");
}, [posts]);
Complete code:
export default function Posts({ userData }) {
const [posts, setPosts] = useState([]);
const getDate = (date) => {
let seconds = date.seconds + date.nanoseconds / 1000000000;
let newDate = new Date(seconds * 1000);
return (
newDate.getDate() +
" " +
newDate.toLocaleString("default", { month: "long" })
);
};
const handleComment = (id) => {
for (let i = 0; i < posts.length; i++) {
if (posts[i].pId === id) {
if (posts[i].expanded === "") posts[i].expanded = id;
else posts[i].expanded = "";
}
}
setPosts(posts);
console.log(posts);
};
useEffect(() => {
console.log("re render");
}, [posts]);
useEffect(() => {
console.log("here");
let parr = [];
const unsub = database.posts
.orderBy("createdAt", "desc")
.onSnapshot((querySnapshot) => {
parr = [];
querySnapshot.forEach((doc) => {
let data = { ...doc.data(), postId: doc.id, expanded: "" };
parr.push(data);
});
setPosts(parr);
});
return unsub;
}, []);
return (
<div style={{ width: "55%" }}>
{!posts || userData == null ? (
<CircularProgress />
) : (
<div className="video-container">
{posts && posts.map((post, index) => {
console.log(post);
return (
<React.Fragment key={index}>
<Card sx={{ maxWidth: 345 }} className="card-class">
<CardHeader
avatar={
<Avatar alt="Remy Sharp" src={userData.profileUrl} />
}
action={
<IconButton aria-label="settings">
<MoreVertIcon />
</IconButton>
}
title={userData.fullname}
subheader={getDate(post.createdAt)}
/>
<Video src={post.pURL} />
<CardActions disableSpacing>
<IconButton aria-label="add to favorites">
<Like userData={userData} postData={post} />
</IconButton>
<IconButton
onClick={() => handleComment(post.pId)}
aria-label="comment"
aria-expanded={post.expanded !== "" ? true : false}
>
<ChatBubbleIcon />
</IconButton>
</CardActions>
<Collapse
in={post.expanded !== "" ? true : false}
timeout="auto"
unmountOnExit
>
<CardContent>
<Typography paragraph>Method:</Typography>
<Typography paragraph>
Heat 1/2 cup of the broth in a pot until simmering, add
saffron and set aside for 10 minutes.
</Typography>
<Typography paragraph>
Heat oil in a (14- to 16-inch) paella pan or a large,
deep skillet over medium-high heat. Add chicken, shrimp
and chorizo, and cook, stirring occasionally until
lightly browned, 6 to 8 minutes. Transfer shrimp to a
large plate and set aside, leaving chicken and chorizo
in the pan. Add pimentón, bay leaves, garlic, tomatoes,
onion, salt and pepper, and cook, stirring often until
thickened and fragrant, about 10 minutes. Add saffron
broth and remaining 4 1/2 cups chicken broth; bring to a
boil.
</Typography>
<Typography paragraph>
Add rice and stir very gently to distribute. Top with
artichokes and peppers, and cook without stirring, until
most of the liquid is absorbed, 15 to 18 minutes. Reduce
heat to medium-low, add reserved shrimp and mussels,
tucking them down into the rice, and cook again without
stirring, until mussels have opened and rice is just
tender, 5 to 7 minutes more. (Discard any mussels that
don&apos;t open.)
</Typography>
<Typography>
Set aside off of the heat to let rest for 10 minutes,
and then serve.
</Typography>
</CardContent>
</Collapse>
</Card>
</React.Fragment>
);
})}
</div>
)}
</div>
);
}
I have also tried using forEach and here posts is getting updated and component is getting re-rendered but posts are not getting displayed. Only CircularProgress is moving.
const handleComment = (id) => {
let newPosts = [...posts]
newPosts = posts.forEach((post) => {
if(post.pId===id)
{
if(post.expanded==="") post.expanded=id;
else post.expanded="";
}
})
setPosts(newPosts);
console.log(posts);
};
I tried setting expanded to non-empty string if it is empty string using handleComment function. It is working fine but the component is not getting re-render as a result expanded remains empty string.
And in the second case of forEach posts state is getting updated and component is also getting re-rendered but only CircularProgress is moving and posts are not getting displayed.

I believe it's not updating because you're mutating the posts array instead of creating a new one. As far as setPosts() is concerned you've passed it the same array so there's no re-render needed (see Updating Arrays in State). You could use map() to create a new array and perform the logic at the same time.
const handleComment = (id) => {
setPosts(posts.map(post => {
if (post.pId === id) {
return {
...post,
expanded: post.expanded ? "" : id
}
} else {
return post
}
}))
};

Related

Unsure why record contents aren't being removed (socket.io)

I'm attempting to create a chat app using socket.io. I've managed to update the rooms whenever the user adds one by means of having a record kept on the back end. However, when I use similar logic, I can't get my button clicking on the front end to remove the room and all those like it on the back end.
Here's my code for the Rooms component (where I generate the contents of my record and show each room and where I have the button and the function to remove the specific room):
export function Rooms() {
useEffect(() => {
socket.emit(EVENTS.CLIENT.ON_LOAD);
}, []);
const { socket, roomId, rooms } = useSockets();
const newRoomRef = useRef<any>(null);
function handleCreateRoom() {
const roomName = newRoomRef.current.value;
// if (!roomName) {
// return;
// }
if (!String(roomName).trim()) return;
socket.emit(EVENTS.CLIENT.CREATE_ROOM, { roomName });
newRoomRef.current.value = "";
}
function handleJoinRoom(key: string) {
if (key === roomId) return;
socket.emit(EVENTS.CLIENT.JOIN_ROOM, key);
}
function handleRemoveRoom(key: string) {
socket.emit(EVENTS.CLIENT.REMOVE_ROOM, key);
newRoomRef.current.value = "";
}
return (
<Box
sx={{
}}
>
<Box
sx={{
}}
>
<TextField
/>
<Button
>
CREATE ROOM
</Button>
</Box>
<Box
>
<Box
>
{Object.keys(rooms).map((key) => {
return (
<Card
>
<Button
disabled={roomId === key}
onClick={() => handleJoinRoom(key)}
sx={{ width: "100%" }}
>
{rooms[key].name}
</Button>
<Button
sx={{}}
onClick={() => handleRemoveRoom(rooms[key].name)}
>
<HighlightOffIcon style={{ color: "red" }} />
</Button>
</Card>
);
})}
</Box>
</Box>
</Box>
);
}
Here's the logic on my back end to remove a room as well as add rooms (I call the add rooms event from the remove room event so I figured I'd show both):
let rooms: Record<string, { name: string }> = {};
socket.on(EVENTS.CLIENT.CREATE_ROOM, ({ roomName }) => {
console.log({ roomName });
// create a roomId
const roomId = nanoid();
// add a new room to the rooms object
rooms[roomId] = {
name: roomName,
};
socket.join(roomId);
// broadcast an event saying there is a new room
socket.broadcast.emit(EVENTS.SERVER.ROOMS, rooms);
// emit back to the room creator with all the rooms
socket.emit(EVENTS.SERVER.ROOMS, rooms);
// emit event back to the room creator saying they have joined a room
socket.emit(EVENTS.SERVER.JOINED_ROOM, roomId);
});
socket.on(EVENTS.CLIENT.REMOVE_ROOM, (value) => {
let deepCopy = { ...rooms };
Object.keys(deepCopy).map((key) => {
if (deepCopy[key].name === value) {
delete deepCopy[key];
}
});
rooms = { ...deepCopy };
socket.emit(EVENTS.SERVER.ROOMS, rooms);
});
And in my context, I'm simply updating the rooms state which is also a record:
socket.on(EVENTS.SERVER.ROOMS, (value) => {
// console.log(value);
setRooms(value);
});
It's my first time working with socket.io, so if you have any other advice, much appreciated.
Just for clarification, adding rooms works, deleting rooms doesn't.

React how to render only first index in array and then filter to display different item in array

I currently have a view button set up to retrieve all the records for a patient that renders all the records in a card I am sending to a component to display them in cards. It also shows all the records for that patient in a select dropdown. I want it to only render the most recent record when I initially click the view button and then be able to re-render the card when a record is selected in the dropdown.
const onViewClick = (diag) => {
diagnosticServices
.getDiagByHorseId(pageData.pageIndex, pageData.pageSize, diag.id)
.then(getDiagByIdSuccess)
.catch(getDiagByIdError);
setToggleDiagSearch(true);
setRenderDiag(!renderDiag);
};
const getDiagByIdSuccess = (response) => {
_logger(response);
let diag = response.item.pagedItems;
setDiagnostics((prevState) => {
const pd = { ...prevState };
pd.arrayOfDiagnostics = diag;
pd.diagComponents = pd.arrayOfDiagnostics.map(diagMapper);
_logger(pd.diagComponents);
return pd;
});
setDateComponents((prevState) => {
const dc = { ...prevState };
dc.arrayOfDates = diag;
dc.dateCreatedComponents = dc.arrayOfDates.map(dateMapper);
_logger(dc.dateCreatedComponents);
return dc;
});
};
const diagMapper = (diag) => {
_logger("Mapping single Diagnostic -->", diag);
return (
<SingleDiagnostic
diagnostic={diag}
key={diag.id}
onEditClick={onEditClick}
></SingleDiagnostic>
);
};
const dateMapper = (diag) => {
_logger("Mapping Diagnostic Dates -->", diag);
return (
<option key={diag.id} value={diag.id}>
{formatDateTime(diag.dateCreated)}
</option>
);
};
I tried to update the selected state value and then filter the state with the selected value, and then to update the render state with the result of the filter, but I am receiving an undefined in my filter. The e.target.value and diag.id are the same id, but the filteredDiag is returning undefined.
const filterDiagnostics = (e) => {
_logger(e.target.value);
const filterDiag = (diag) => {
_logger(diag.id);
let result = false;
if (e.target.value === diag.id) {
result = true;
} else {
result = false;
}
return result;
};
let filteredDiag = diagnostics.arrayOfDiagnostics.filter(filterDiag);
_logger(filteredDiag);
setDiagnostics((prevState) => {
let diagnostic = { ...prevState };
diagnostic = filteredDiag.map(diagMapper);
return diagnostic;
});
setRenderDiag(!renderDiag);
};
In my return statement:
<Col xs={8}>
{toggleDiagSearch && (
<Card className="mt-4">
<Form onSubmit={handleSubmit}>
<Card.Header>
<strong> Diagnostics History</strong>
<Field
as="select"
className="form-select"
name="diagComponents"
onChange={filterDiagnostics}
value={values.id}
>
<option label="Select" value="select"></option>
{dateComponents.dateCreatedComponents}
</Field>
</Card.Header>
</Form>
</Card>
)}
<Card className="mt-3">
{renderDiag && diagnostics.diagComponents}
</Card>
</Col>
I tried to set the diagComponents[0] to initially display only the first index, but I don't think that will let me display the other records when I select a new record from the dropdown. Where do I have to make adjustments to show only the first record and get my filter to function correctly?

Is it Possible to Dynamically Generate Dropdown Items?

I'm working in react, and for one part of the project I need x amount of dropdown menus. Each dropdown menu will have y amount of options to choose from.
Usually with things like this, I can just use a simple map function and I am good to go, however the syntax gets a little tricky since one DropdownMenu has many Dropdown.Items. My code looks like this, no errors pop up and the console.log statements return exactly what is to be expected, but absolutely nothing renders.
const renderDefaultScheduling = () => {
return allDevices.map( (device, superIndex) => {
<Card>
{renderAllOfThisDevice(device, superIndex)}
</Card>
})
}
const renderAllOfThisDevice = (deviceObj, superIndex) => {
let oneDeviceDropdowns = []
console.log(deviceObj)
for (let i = 1; i <= deviceObj.amount; i = i + 1){
let thisOptions = renderDropDownOptionsFromList(deviceObj, superIndex)
oneDeviceDropdowns.push(
<DropdownButton title={deviceObj[i]}>
{thisOptions}
</DropdownButton>
)
}
return(
<Card>
<Card.Title>{deviceObj.name}</Card.Title>
<Card.Body>
{oneDeviceDropdowns}
</Card.Body>
</Card>
)
}
const renderDropDownOptionsFromList = (deviceObj, superIndex) => {
console.log(deviceObj)
return deviceObj.remaining_drivers.map( (driver, index) => {
<Dropdown.Item key={index} onClick={() => handleDriverSelection(deviceObj, driver, index, superIndex)}>
{driver.firstname} {driver.lastname}
</Dropdown.Item>
})
}
What gets me, is not even the <Card.Title>{deviceObj.name}</Card.Title> line renders, which is not inside the nested map, only the first layer of it... So if deviceObj is logging properly, I see legitimately no reason why that line wouldn't be rendering. Does anyone have any ideas, am I evenm able to do this with DropDown Menus?
no data is showing beacuse you are not returning it from the map function callback in renderDefaultScheduling and renderDropDownOptionsFromList
i have marked the return statement with return
const renderDefaultScheduling = () => {
return allDevices.map( (device, superIndex) => {
****return**** <Card>
{renderAllOfThisDevice(device, superIndex)}
</Card>
})
}
const renderAllOfThisDevice = (deviceObj, superIndex) => {
let oneDeviceDropdowns = []
console.log(deviceObj)
for (let i = 1; i <= deviceObj.amount; i = i + 1){
let thisOptions = renderDropDownOptionsFromList(deviceObj, superIndex)
oneDeviceDropdowns.push(
<DropdownButton title={deviceObj[i]}>
{thisOptions}
</DropdownButton>
)
}
return(
<Card>
<Card.Title>{deviceObj.name}</Card.Title>
<Card.Body>
{oneDeviceDropdowns}
</Card.Body>
</Card>
)
}
const renderDropDownOptionsFromList = (deviceObj, superIndex) => {
console.log(deviceObj)
return deviceObj.remaining_drivers.map( (driver, index) => {
****return**** <Dropdown.Item key={index} onClick={() => handleDriverSelection(deviceObj, driver, index, superIndex)}>
{driver.firstname} {driver.lastname}
</Dropdown.Item>
})
}

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

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.

How to render AdMob banner in React Flatlist between items?

I have a React Native Flatlist that only re-renders when its data has changed.
I give it the following data (as prop):
const posts = [
{
...post1Data
},
{
...post2Data
},
{
...post3Data
},
{
...post4Data
},
{
...post5Data
},
]
And here is my FlatList renderItem:
const renderItem = useCallback(({ item, index }) => {
const { id, userData, images, dimensions, text } = item;
return (
<View
onLayout={(event) => {
itemHeights.current[index] = event.nativeEvent.layout.height;
}}
>
<Card
id={id}
cached={false}
userData={userData}
images={images}
dimensions={dimensions}
text={text}
/>
</View>
);
}, []);
How can I add an AdMob ad between the FlatList data with a probability of 5% without skiping any data in the posts array?
I have tried this:
const renderItem = useCallback(({ item, index }) => {
const { id, userData, images, dimensions, text } = item;
if (Math.random() < 0.05) return <Ad ... />
return (
<View
onLayout={(event) => {
itemHeights.current[index] = event.nativeEvent.layout.height;
}}
>
<Card
id={id}
cached={false}
userData={userData}
images={images}
dimensions={dimensions}
text={text}
/>
</View>
);
}, []);
But this causes 2 problems:
Some items from data are skipped (not returned)
When the flatlist re-renders (because of some of its props changes) the ads might disappear (there is a chance of 95%).
Any ideas? Should I render the ads randomly in the footer of my Card component like this?
const Card = memo ((props) => {
...
return (
<AuthorRow ... />
<Content ... />
<SocialRow ... /> {/* Interaction buttons */}
<AdRow />
)
}, (prevProps, nextProps) => { ... });
const AdRow = memo(() => {
return <Ad ... />
}, () => true);
I am not really sure about this option, it works but it could violate the admob regulations (?) (because I am adapting the ad to the layout of my card component)
I would appreciate any kind of guidance/help. Thank you.
I'm not sure if you ever found a solution to this problem, but I accomplished this by injecting "dummy" items into the data set, then wrapping the renderItem component with a component that switches based on the type of each item.
Assuming your flatlist is declared like this:
<FlatList data={getData()} renderItem={renderItem}/>
And your data set is loaded into a variable called sourceData that is tied to state. Let's assume one entry in your sourceData array looks like this. Note the 'type' field to act as a type discriminator:
{
"id": "d96dce3a-6034-47b8-aa45-52b8d2fdc32f",
"name": "Joe Smith",
"type": "person"
}
Then you could declare a function like this:
const getData = React.useCallback(() => {
let outData = [];
outData.push(...sourceData);
// Inject ads into array
for (let i = 4; i < outData.length; i += 5)
{
outData.splice(i, 0, {type:"ad"});
}
return outData;
}, [sourceData]);
... which will inject ads into the data array between every 4th item, beginning at the 5th item. (Since we're pushing new data into the array, i += 5 means an ad will be placed between every 4th item. And let i = 4 means our first ad will show after the 5th item in our list)
Finally, switch between item types when you render:
const renderItem = ({ item }) => (
item.type === 'ad'
?
<AdComponent ...props/>
:
<DataComponent ...props/>
);

Categories