In Instagram clone project, I created comment box for each user, submitting comments works properly but, inputting text shows text for all users. Following is the code required.
Home.js
const [data, setData] = useState([])
const [userProfile, setUserProfile] = useState(null)
const {state, dispatch} = useContext(UserContext)
/* Comment function */
const makeComment = (text,postId)=>{
fetch('/comment',{
method:"put",
headers:{
"Content-Type":"application/json",
"Authorization":"Bearer "+localStorage.getItem("jwt")
},
body:JSON.stringify({
postId,
text
})
}).then(res=>res.json())
.then(result=>{
console.log(result)
const newData = data.map(item=>{
if(item._id==result._id){
return {...item,comments: result.comments}
}else{
return item
}
})
setData(newData)
}).catch(err=>{
console.log(err)
})
}
return (
<div className="home" >
{
data.map(item => {
return (
<div className="card home-card" key={item._id}>
<div className="profile-card">
/* Profile Image */
<img style={{width: '50px', height:'50px', borderRadius:'80px'}}
src={item.postedBy.image} />
<span style={{display:'flex', flexWrap:'wrap', width:'85%'}}>
/* Profile Link */
<h5><Link to={item.postedBy._id !== state._id ? "/profile/"+item.postedBy._id : "/profile" } > {item.postedBy.name} </Link></h5>
</span>
</div>
/* Post Image */
<div className="card-image">
<img style={{width: '100%', height:'260px'}} src={item.photo} alt=""/>
</div>
<hr/>
/* Like Button */
<div className="like-section">
{ item.likes.includes(state._id) ?
<FavoriteIcon className="like-heart" style={{ fontSize: 25 }} onClick={()=>{unlikePost(item._id)}} /> :
<FavoriteBorderIcon className="unlike-heart" style={{ fontSize: 25 }} onClick={()=>{likePost(item._id)}} />
}
<h6>{item.likes.length}</h6>
</div>
<br/>
/* Comments Section */
<div className="card-content">
<h6><b>{item.title}</b></h6>
<p>{item.body}</p>
{
item.comments.map(record => {
return (
<>
<h6 key={record._id}><b className="posted-by">{record.postedBy.name}</b>{record.text}</h6>
</>
)
})
}
{
/* Comment Box */
<form onSubmit={(e) => {
e.preventDefault();
makeComment(e.target[0].value, item._id)
setNewText("")
}}>
<input type="text" value={newtext} onChange={onChange} placeholder="add a comment" />
<button type="submit" disabled={!newtext}>Post</button>
</form>
}
</div>
</div>
)
})
}
</div>
);
};
export default Home;
When typing comments in particular comment box only that should be focused and display text, but all other comments are displayed
Below is the image reference
Above image you can see 'Hello' text when typed is displayed in both comment box
Here you can see 'Hello' text is submitted properly to the that particular id
So, any appropriate solution?
That's because you are using the same state for each comment boxes.
To overcome this, you need to create as many state as users.
You can do this easily by using an array for comments
const [newText, setNewText] = useState([])
data.map((item, index) => (
...
<input value={newText[index]} onChange={(e) => setNewText(replaceByIndex(newText, index, e.target.value))} />
...
)
FYI, here's replaceByIndex function:
const replaceByIndex = (originArray, index, newItem) =>
originArray.map((item, i) => i === index ? newItem: item)
Additional enhancements to your code:
Performance enhancements: use useCallback react hook to define functions in function components.
Use meaningful naming conventions. It's hard to understand what the data and item means...
Related
How can I Change styles on hover of an appropriate item react? Now the styles are changed of all of the items at a time. Hovering on add to cart button I need to change only the chosen div styles.
https://codesandbox.io/s/trusting-moon-djocul?file=/src/components/Category.js**
import React, { useState } from "react";
import styles from "./category.module.css";
import Categories from "./Categories";
const Category = () => {
const [hovered, setHovered] = useState(false);
const [data, setData] = useState(Categories);
const style = hovered
? { backgroundColor: "#ffcbcb", color: "#fff", transition: "all 0.5s ease" }
: {};
const filterResult = (catItem) => {
const result = Categories.filter((curData) => curData.category === catItem);
setData(result);
};
return (
<>
<div className={styles.items}>
{data.map((value) => {
const { id, title, price, description } = value;
return (
<>
<div className={styles.item} key={id} style={style}>
<h1>{title}</h1>
<p>
{price} <span>$</span>
</p>
<p>{description}</p>
<button
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className={styles.btn}
>
Add to Cart
</button>
</div>
</>
);
};
export default Category;
That's because you have a single "hovered" state shared between all your cards, you should create a "Card" component and have that component have its own hovered state
return (
<>
<div className={styles.items}>
{data.map((value) => {
return (
<>
<Card {...props} />
</>
);
Problem
This issue is occurring cause you are applying CSS to all the cards. The only small thing you are missing in your logic is adding CSS to the only card whose button was being hovered.
Solution
That can be achieved by using event objects on mouse enter and mouse leave events.
<div className={styles.item} key={id} style={style}>
<h1>{title}</h1>
<p>
{price} <span>$</span>
</p>
<p>{description}</p>
<button
onMouseEnter={(e) =>
e.currentTarget.parentElement.classList.add("active_card")
}
onMouseLeave={(e) =>
e.currentTarget.parentElement.classList.remove("active_card")
}
className={styles.btn}
>
Add to Cart
</button>
</div>
This will add a class of "active_card" on the card whose Add To Card button is being hovered, then you can apply your desired styling to that class.
.active_card {
background-color: #ffcbcb !important;
}
Example
Working Code Sandbox Example
I have a list of users and I want to display in another component on the same page the user data in input fields for every user that I click on.
When no user is selected, I want the component to just render some text and a button to add a user. When the button is clicked the component renders the form with empty input fields so that we can add a new user.
I tried the following, but it's just showing the data for the first one I click on. It's not updating.
The main page:
const index = (props) => {
const [selectedUser, setSelectedUser] = useState(null);
const [state, setState] = useState("Index");
const onChange = (item) => {
setState("Edit");
setSelectedUser(item);
};
const onClick = (e, item) => {
if (e.type === "click" && e.clientX !== 0 && e.clientY !== 0) {
onChange(item);
} else {
console.log('prevented "onClick" on keypress');
}
};
const renderComponent = () => {
switch (state) {
case "Index":
return (
<>
<div className="btn" onClick={(e) => setState("Edit")}>
+ New Staff
</div>
<img src="/storage/illustrations/collaboration.svg" />
</>
);
case "Edit":
return (
<div>
<StaffForm profile={selectedUser} />
</div>
);
}
};
return (
<>
<div>
<div>
<h1>Staff</h1>
</div>
<div>
<div>
{profiles.map((item, index) => {
return (
<div key={index} onClick={(e) => onClick(e, item)}>
<input
type={"radio"}
name={"staff"}
checked={state === item}
onChange={(e) => onChange(item)}
/>
<span>{item.user.name}</span>
</div>
);
})}
</div>
<div>{renderComponent()}</div>
</div>
</div>
</>
);
};
The Staff Form Component:
const StaffForm = ({ profile }) => {
const { data, setData, post, processing, errors, reset } = useForm({
email: profile ? profile.user.email : "",
name: profile ? profile.user.name : "",
phone_number: profile ? profile.user.phone_number : "",
avatar: profile ? profile.user.avatar : "",
});
const [file, setFile] = useState(data.avatar);
const handleImageUpload = (e) => {
setFile(URL.createObjectURL(e.target.files[0]));
setData("avatar", e.target.files[0]);
};
const onHandleChange = (event) => {
setData(
event.target.name,
event.target.type === "checkbox"
? event.target.checked
: event.target.value
);
};
return (
<div>
<ImageUpload
name={data.name}
file={file}
handleImageUpload={handleImageUpload}
/>
<TextInput
type="text"
name="name"
value={data.name}
autoComplete="name"
isFocused={true}
onChange={onHandleChange}
placeholder={t("Name")}
required
/>
<TextInput
type="text"
name="phone_number"
value={data.phone_number}
autoComplete="phone_number"
placeholder={t("Phone Number")}
onChange={onHandleChange}
required
/>
<TextInput
type="email"
name="email"
value={data.email}
autoComplete="email"
onChange={onHandleChange}
placeholder={t("Email")}
required
/>
</div>
);
};
First of all something you should avoid is the renderComponent() call.Check here the first mistake mentioned in this video. This will most likely fix your problem but even if it doesn't the video explains why it should not be used.
Something else that caught my eye(possibly unrelated to your question but good to know) is the onChange function. When two pieces of state change together it is a potential source of problems, check out this article on when to use the useReducer hook.
Also be careful with naming React Components, they need to be capital case, this question contains appropriate answers explaining it.
(To only solve your problem stick to no.1 of this list, there are some improvements i'd do here overall for code quality and beauty, msg me for more details)
I have a component call ExpenseList.js which does look like below. But my problem is when I tried to edit item and click save, setting isEditable inside "Save" button event handler does not trigger re-render.
import { useState } from "react";
import { useExpenses, useExpenseDispatch } from "./BudgetContext.js";
export default function ExpenseList() {
const expenses = useExpenses();
return (
<ul>
{expenses.map((expense) => (
<li key={expense.id}>
<Expense expense={expense} />
</li>
))}
</ul>
);
}
function Expense({ expense }) {
const [isEditing, setIsEditing] = useState(false);
const dispatch = useExpenseDispatch();
let content;
if (isEditing) {
content = (
<>
<input
value={expense.description}
onChange={(e) => {
dispatch({
type: "changed",
expense: {
...expense,
description: e.target.value
}
});
}}
/>
<input
value={expense.amount}
onChange={(e) => {
dispatch({
type: "changed",
expense: {
...expense,
amount: e.target.value
}
});
}}
/>
<button onClick={() => setIsEditing(false)}>Save</button>
</>
);
} else
content = (
<>
<span>{expense.description}</span> : <span>{expense.amount}</span>
<button onClick={() => setIsEditing(true)}>Edit</button>
</>
);
return (
<label>
{content}
<button
onClick={() => {
dispatch({
type: "deleted",
id: expense.id
});
}}
>
Delete
</button>
</label>
);
}
I was dabbling with this for hours, I think extra pair of eyes could point out what is going wrong?
Sandbox: https://codesandbox.io/s/clever-keller-l5z42e?file=%2FExpenseList.js%3A0-1614
Documentation reference
Content model:
Phrasing content, but with no descendant labelable elements unless it is the element's labeled control, and no descendant label elements.
As mentioned above, label has 2 different labelable elements unless it is the element's labeled control. When you are in edit mode, you have 3 different labelable elements (input-description, input-amount and button-save) which causes problems with event propagation.
But when you are not in edit mode, it just has 1 labelable element which is the edit button and hence, it works.
For solving your issue, you can swap the label at the root with something like div and then use labels explicitly for each of the inputs in content.
function Expense({ expense }) {
const [isEditing, setIsEditing] = useState(false);
let content;
if (isEditing) {
content = (
<>
<label>
Description:
<input
value={expense.description}
onChange={...}
/>
</label>
<label>
Amount:
<input
value={expense.amount}
onChange={...}
/>
</label>
<button onClick={() => setIsEditing(false)}>Save</button>
</>
);
} else
content = (
<>
<button onClick={() => setIsEditing(true)}>Edit</button>
</>
);
return (
<div>
{content}
<button>Delete</button>
</div>
);
}
This is a react project that uses an API to create a card which presents a Github users information. At the moment I am implementing a delete button on each card that would delete that specific card. You can try the application on code sandbox by clicking here.. Instructions how to use: enter any 'Github' username into the input and click add card.
Once you click on add card, the user information is extracted into a Card which is then stored under CardList. This has an identifiable by key. But when clicking on Delete, instead of deleting the corresponding card to where the delete button is pressed, currently all the cards are deleted. (I have excluded the form component to make it easier to read).
Not sure if I am incorrectly using splice or not correctly declaring the cards key?
function App() {
const [cards, setCards] = useState([]);
const addNewCard = cardInfo => {
setCards(cards.concat(cardInfo));
};
const removeCard = key => {
setCards(cards.splice(key, 0))
}
return (
<div>
<Form onSubmit={addNewCard} />
<CardList
cards={cards}
handleClick={removeCard}
/>
</div>
);
}
const CardList = props => (
<div>
{props.cards.map((card, index) => (
<Card {...card} key={index} handleClick={props.handleClick} />
))}
</div>
);
const Card = props => {
return (
<div style={{ margin: "1em" }}>
<img alt="avatar" style={{ width: "70px" }} src={props.avatar_url} />
<div>
<div style={{ fontWeight: "bold" }}>{props.name}</div>
<div>{props.blog}</div>
<a href={props.html_url}>{props.html_url}</a>
<button onClick={props.handleClick}>Delete</button>
</div>
</div>
);
};
To be exact you incorretly use splice in the context of hooks. Check here to know how splice works
The splice() method adds/removes items to/from an array, and returns the removed item(s).
So you are setting the element you try to remove to your variable. To keep your original logic, I suggest you use a temp variable like this:
const removeCard = key => {
let tempCards = cards;
const removedCard = tempCards.splice(key, 0); // you can use the removed card if needed
setCards(tempCards);
}
And the parameter key is not defined you have to pass this parameter to your function see the documentation :
For just minor change from your code, you can change this:
<Card {...card} key={index} handleClick={props.handleClick} />
to this:
<Card {...card} key={index} handleClick={() => props.handleClick(index)} />
EDIT: When you use concat maybe, it's adding each element of your new card. Try to change this :
setCards(cards.concat(cardInfo));
to this:
setCards(cards.push(cardInfo));
My error here was using splice instead of a slice. Since splice mutates the original array and that should be avoided. But by slicing the array up to the index which you would like to remove, you can then slice from the other side of the index that you would like to delete and concat the second slice to the first.
const removeCard = key => {
setCards(cards.slice(0, key).concat(cards.slice(key + 1, cards.length)))
};
You can find the full code implementation below:
function App() {
const [cards, setCards] = useState([]);
const addNewCard = cardInfo => {
setCards(cards.concat(cardInfo));
};
const removeCard = key => {
setCards(cards.slice(0, key).concat(cards.slice(key + 1, cards.length)))
};
return (
<div>
<Form onSubmit={addNewCard} />
<CardList
cards={cards}
handleClick={removeCard}
/>
</div>
);
}
const CardList = props => (
<div>
{props.cards.map((card, index) => (
<Card {...card} key={index} handleClick={props.handleClick(index)} />
))}
</div>
);
const Card = props => {
return (
<div style={{ margin: "1em" }}>
<img alt="avatar" style={{ width: "70px" }} src={props.avatar_url} />
<div>
<div style={{ fontWeight: "bold" }}>{props.name}</div>
<div>{props.blog}</div>
<a href={props.html_url}>{props.html_url}</a>
<button onClick={props.handleClick}>Delete</button>
</div>
</div>
);
};
I'm attempting to use a .put call to edit a color from an array of colors that I am pulling from an API. However, my .put call is not working for some reason. I am unable to get the response from the .put call to log to the console. When I try to submit by clicking the save button, I get an error that says colors.map is not a function. Does anyone know how I can resolve this?
import React, { useState } from "react";
import axios from "axios";
import { axiosWithAuth } from "../utils/axiosWithAuth";
const initialColor = {
color: "",
code: { hex: "" },
};
const ColorList = ({ colors, updateColors }) => {
console.log(colors);
const [editing, setEditing] = useState(false);
const [colorToEdit, setColorToEdit] = useState(initialColor);
const editColor = color => {
setEditing(true);
setColorToEdit(color);
};
const saveEdit = e => {
e.preventDefault();
// Make a put request to save your updated color
// think about where will you get the id from...
// where is is saved right now?
axiosWithAuth().put(`/colors/${colorToEdit.id}`, colorToEdit)
.then(res => {
console.log(res);
updateColors(res.data);
})
};
const deleteColor = color => {
// make a delete request to delete this color
};
return (
<div className="colors-wrap">
<p>colors</p>
<ul>
{colors.map(color => (
<li key={color.color} onClick={() => editColor(color)}>
<span>
<span className="delete" onClick={e => {
e.stopPropagation();
deleteColor(color)
}
}>
x
</span>{" "}
{color.color}
</span>
<div
className="color-box"
style={{ backgroundColor: color.code.hex }}
/>
</li>
))}
</ul>
{editing && (
<form onSubmit={saveEdit}>
<legend>edit color</legend>
<label>
color name:
<input
onChange={e =>
setColorToEdit({ ...colorToEdit, color: e.target.value })
}
value={colorToEdit.color}
/>
</label>
<label>
hex code:
<input
onChange={e =>
setColorToEdit({
...colorToEdit,
code: { hex: e.target.value }
})
}
value={colorToEdit.code.hex}
/>
</label>
<div className="button-row">
<button type="submit">save</button>
<button onClick={() => setEditing(false)}>cancel</button>
</div>
</form>
)}
<div className="spacer" />
{/* stretch - build another form here to add a color */}
</div>
);
};
export default ColorList;
if you are getting error "that says colors.map is not a function" mean colors in not an array type. You may check using Array.isArray(colors). It will return true if colors will array type.